diff --git a/packages/audiofileplayer/.gitignore b/packages/audiofileplayer/.gitignore new file mode 100644 index 0000000..e9dc58d --- /dev/null +++ b/packages/audiofileplayer/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/audiofileplayer/.metadata b/packages/audiofileplayer/.metadata new file mode 100644 index 0000000..2c8ec56 --- /dev/null +++ b/packages/audiofileplayer/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 8661d8aecd626f7f57ccbcb735553edc05a2e713 + channel: stable + +project_type: plugin diff --git a/packages/audiofileplayer/CHANGELOG.md b/packages/audiofileplayer/CHANGELOG.md new file mode 100644 index 0000000..61c5173 --- /dev/null +++ b/packages/audiofileplayer/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 - 18 March 2019 + + * Initial open source release diff --git a/packages/audiofileplayer/LICENSE b/packages/audiofileplayer/LICENSE new file mode 100644 index 0000000..0ab80a7 --- /dev/null +++ b/packages/audiofileplayer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Google Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/audiofileplayer/README.md b/packages/audiofileplayer/README.md new file mode 100644 index 0000000..4d8280c --- /dev/null +++ b/packages/audiofileplayer/README.md @@ -0,0 +1,25 @@ +# audiofileplayer + +A Flutter plugin for audio playback. +Supports + * Reading audio data from Flutter project assets, byte arrays, and remote URLs. + * Seek to position. + * Continue playback while app is backgrounded. + * Callbacks for loaded audio duration, current position, and playback completion. + * Volume + * Looping + * Pause/Resume. + * Multiple audio players, with automatic memory management. + +## Getting Started + +To use this plugin, add `audiofileplayer` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). + +### Example + +``` dart +// Play a sound as a one-shot, releasing its resources when it finishes playing. +Audio.load('assets/foo.wav')..play()..dispose(); +``` + +Please see the header comment of audiofileplayer.dart for more information, and see the example app of this plugin for an example showing multiple use cases. diff --git a/packages/audiofileplayer/android/.gitignore b/packages/audiofileplayer/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/packages/audiofileplayer/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/packages/audiofileplayer/android/build.gradle b/packages/audiofileplayer/android/build.gradle new file mode 100644 index 0000000..bba39db --- /dev/null +++ b/packages/audiofileplayer/android/build.gradle @@ -0,0 +1,39 @@ +group 'com.google.flutter.plugins.audiofileplayer' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.2.1' + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + } +} diff --git a/packages/audiofileplayer/android/gradle.properties b/packages/audiofileplayer/android/gradle.properties new file mode 100644 index 0000000..8bd86f6 --- /dev/null +++ b/packages/audiofileplayer/android/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1536M diff --git a/packages/audiofileplayer/android/settings.gradle b/packages/audiofileplayer/android/settings.gradle new file mode 100644 index 0000000..87b1ed0 --- /dev/null +++ b/packages/audiofileplayer/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'audiofileplayer' diff --git a/packages/audiofileplayer/android/src/main/AndroidManifest.xml b/packages/audiofileplayer/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..28ead18 --- /dev/null +++ b/packages/audiofileplayer/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/AudiofileplayerPlugin.java b/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/AudiofileplayerPlugin.java new file mode 100644 index 0000000..939c31e --- /dev/null +++ b/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/AudiofileplayerPlugin.java @@ -0,0 +1,212 @@ +package com.google.flutter.plugins.audiofileplayer; + +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.util.Log; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry.Registrar; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Flutter audio file player plugin. + * + *

Receives messages which create, trigger, and destroy instances of {@link ManagedMediaPlayer}. + */ +public class AudiofileplayerPlugin implements MethodCallHandler { + private static final String TAG = AudiofileplayerPlugin.class.getSimpleName(); + + // Method channel constants, matching those in the Dart and iOS plugin code. + private static final String CHANNEL = "audiofileplayer"; + private static final String LOAD_METHOD = "load"; + private static final String FLUTTER_PATH = "flutterPath"; + private static final String AUDIO_BYTES = "audioBytes"; + private static final String REMOTE_URL = "remoteUrl"; + private static final String AUDIO_ID = "audioId"; + private static final String LOOPING = "looping"; + private static final String RELEASE_METHOD = "release"; + private static final String PLAY_METHOD = "play"; + private static final String PLAY_FROM_START = "playFromStart"; + private static final String SEEK_METHOD = "seek"; + private static final String SET_VOLUME_METHOD = "setVolume"; + private static final String VOLUME = "volume"; + private static final String PAUSE_METHOD = "pause"; + private static final String ON_COMPLETE_CALLBACK = "onComplete"; + private static final String ON_DURATION_CALLBACK = "onDuration"; + private static final String DURATION_SECONDS = "duration_seconds"; + private static final String ON_POSITION_CALLBACK = "onPosition"; + private static final String POSITION_SECONDS = "position_seconds"; + private static final String ERROR_CODE = "AudioPluginError"; + + private final Registrar registrar; + private final Map mediaPlayers; + private final MethodChannel methodChannel; + + public static void registerWith(Registrar registrar) { + final MethodChannel methodChannel = + new MethodChannel(registrar.messenger(), CHANNEL); + final AudiofileplayerPlugin instance = + new AudiofileplayerPlugin(registrar, methodChannel); + methodChannel.setMethodCallHandler(instance); + } + + private AudiofileplayerPlugin(Registrar registrar, MethodChannel methodChannel) { + this.registrar = registrar; + this.methodChannel = methodChannel; + this.mediaPlayers = new HashMap<>(); + } + + @Override + public void onMethodCall(MethodCall call, Result result) { + Log.i(TAG, "onMethodCall: method = " + call.method); + if (call.method.equals(LOAD_METHOD)) { + onLoad(call, result); + return; + } + // All subsequent calls need a valid player. + ManagedMediaPlayer player = getAndVerifyPlayer(call, result); + if (call.method.equals(PLAY_METHOD)) { + Boolean playFromStartBoolean = call.argument(PLAY_FROM_START); + boolean playFromStart = playFromStartBoolean.booleanValue(); + player.play(playFromStart); + result.success(null); + } else if (call.method.equals(RELEASE_METHOD)) { + player.release(); + mediaPlayers.remove(player.getAudioId()); + result.success(null); + } else if (call.method.equals(SEEK_METHOD)) { + Double positionSecondsDouble = call.argument(POSITION_SECONDS); + double positionSeconds = positionSecondsDouble.doubleValue(); + player.seek(positionSeconds); + result.success(null); + } else if (call.method.equals(SET_VOLUME_METHOD)) { + Double volumeDouble = call.argument(VOLUME); + double volume = volumeDouble.doubleValue(); + player.setVolume(volume); + result.success(null); + } else if (call.method.equals(PAUSE_METHOD)) { + player.pause(); + result.success(null); + } else { + result.notImplemented(); + } + } + + private void onLoad(MethodCall call, Result result) { + String audioId = call.argument(AUDIO_ID); + if (audioId == null) { + result.error(ERROR_CODE, "Received load() call without an audioId", null); + return; + } + if (mediaPlayers.get(audioId) != null) { + result.error( + ERROR_CODE, "Tried to load an already-loaded player: " + audioId, null); + return; + } + + Boolean loopingBoolean = call.argument(LOOPING); + boolean looping = false; + if (loopingBoolean != null) { + looping = loopingBoolean.booleanValue(); + } + + try { + if (call.argument(FLUTTER_PATH) != null) { + String flutterPath = call.argument(FLUTTER_PATH).toString(); + AssetManager assetManager = registrar.context().getAssets(); + String key = registrar.lookupKeyForAsset(flutterPath); + AssetFileDescriptor fd = assetManager.openFd(key); + ManagedMediaPlayer newPlayer = new LocalManagedMediaPlayer(audioId, fd, this, looping); + mediaPlayers.put(audioId, newPlayer); + handleDurationForPlayer(newPlayer, audioId); + result.success(null); + } else if (call.argument(AUDIO_BYTES) != null) { + byte[] audioBytes = call.argument(AUDIO_BYTES); + ManagedMediaPlayer newPlayer = + new LocalManagedMediaPlayer( + audioId, new BufferMediaDataSource(audioBytes), this, looping); + mediaPlayers.put(audioId, newPlayer); + handleDurationForPlayer(newPlayer, audioId); + result.success(null); + } else if (call.argument(REMOTE_URL) != null) { + String remoteUrl = call.argument(REMOTE_URL); + // Note that this will throw an exception on invalid URL or lack of network connectivity. + RemoteManagedMediaPlayer newPlayer = + new RemoteManagedMediaPlayer(audioId, remoteUrl, this, looping); + newPlayer.setOnRemoteLoadListener( + (success) -> { + if (success) { + handleDurationForPlayer(newPlayer, audioId); + result.success(null); + } else { + mediaPlayers.remove(audioId); + result.error( + ERROR_CODE, + "Remote URL loading failed for URL: " + remoteUrl, + null); + } + }); + // Add player to data structure immediately; will be removed if async loading fails. + mediaPlayers.put(audioId, newPlayer); + } else { + result.error( + ERROR_CODE, + "Could not create ManagedMediaPlayer with no flutterPath, audioBytes, nor remoteUrl.", + null); + return; + } + } catch (Exception e) { + result.error( + ERROR_CODE, "Could not create ManagedMediaPlayer:" + e.getMessage(), null); + } + } + + private ManagedMediaPlayer getAndVerifyPlayer(MethodCall call, Result result) { + String audioId = call.argument(AUDIO_ID); + if (audioId == null) { + result.error( + ERROR_CODE, + String.format("Received %s call without an audioId", call.method), + null); + return null; + } + ManagedMediaPlayer player = mediaPlayers.get(audioId); + if (player == null) { + result.error( + ERROR_CODE, + String.format("Called %s on an unloaded player: %s", call.method, audioId), + null); + } + return player; + } + + /** Called by {@link ManagedMediaPlayer} when (non-looping) file has finished playback. */ + public void handleCompletion(String audioId) { + this.methodChannel.invokeMethod( + ON_COMPLETE_CALLBACK, + Collections.singletonMap(AUDIO_ID, audioId)); + } + + // Called on successful load. + public void handleDurationForPlayer(ManagedMediaPlayer player, String audioId) { + Map arguments = new HashMap(); + arguments.put(AUDIO_ID, audioId); + // Note that player will report a negative value if duration is unavailable (for example, + // streaming certain types of remote audio). + double durationSeconds = player.getDurationSeconds(); + arguments.put(DURATION_SECONDS, Double.valueOf(durationSeconds)); + this.methodChannel.invokeMethod(ON_DURATION_CALLBACK, arguments); + } + + /** Called repeatedly by {@link ManagedMediaPlayer} during playback. */ + public void handlePosition(String audioId, double positionSeconds) { + Map arguments = new HashMap(); + arguments.put(AUDIO_ID, audioId); + arguments.put(POSITION_SECONDS, Double.valueOf(positionSeconds)); + this.methodChannel.invokeMethod(ON_POSITION_CALLBACK, arguments); + } +} diff --git a/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/BufferMediaDataSource.java b/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/BufferMediaDataSource.java new file mode 100644 index 0000000..34973d3 --- /dev/null +++ b/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/BufferMediaDataSource.java @@ -0,0 +1,32 @@ +package com.google.flutter.plugins.audiofileplayer; + +import android.media.MediaDataSource; +import java.io.IOException; + +/** A MediaDataSource implementation to read a byte array of media data. */ +final class BufferMediaDataSource extends MediaDataSource { + private final byte[] bytes; + + public BufferMediaDataSource(byte[] bytes) { + this.bytes = bytes; + } + + @Override + public long getSize() { + return bytes.length; + } + + @Override + public int readAt(long position, byte[] buffer, int offset, int size) { + if (position >= bytes.length) { + // Indicate end of stream with -1. + return -1; + } + int readSize = Math.min(size, Math.min(buffer.length - offset, bytes.length - (int) position)); + System.arraycopy(bytes, (int) position, buffer, offset, readSize); + return readSize; + } + + @Override + public void close() throws IOException {} +} diff --git a/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/LocalManagedMediaPlayer.java b/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/LocalManagedMediaPlayer.java new file mode 100644 index 0000000..2868be5 --- /dev/null +++ b/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/LocalManagedMediaPlayer.java @@ -0,0 +1,64 @@ +package com.google.flutter.plugins.audiofileplayer; + +import android.content.res.AssetFileDescriptor; +import android.media.MediaPlayer; +import java.io.IOException; + +/** + * Wraps a MediaPlayer for local asset use by AudiofileplayerPlugin. + * + *

Used for local audio data only; loading occurs synchronously. Loading remote audio should use + * ManagedRemoteMediaPlayer. + */ +class LocalManagedMediaPlayer extends ManagedMediaPlayer { + + /** + * Private shared constructor. + * + *

Callers must subsequently set a data source and call {@link MediaPlayer#prepare()}. + */ + private LocalManagedMediaPlayer( + String audioId, + AudiofileplayerPlugin parentAudioPlugin, + boolean looping) + throws IllegalArgumentException, IOException { + super(audioId, parentAudioPlugin); + player = new MediaPlayer(); + player.setLooping(looping); + player.setOnErrorListener(this); + player.setOnCompletionListener(this); + } + + /** + * Create a LocalManagedMediaPlayer from an AssetFileDescriptor. + * + * @throws IOException if underlying MediaPlayer cannot load AssetFileDescriptor. + */ + public LocalManagedMediaPlayer( + String audioId, + AssetFileDescriptor fd, + AudiofileplayerPlugin parentAudioPlugin, + boolean looping) + throws IOException { + this(audioId, parentAudioPlugin, looping); + player.setDataSource(fd); + player.prepare(); + } + + /** + * Create a ManagedMediaPlayer from an BufferMediaDataSource. + * + * @throws IllegalArgumentException if BufferMediaDataSource is invalid. + * @throws IOException if underlying MediaPlayer cannot load BufferMediaDataSource. + */ + public LocalManagedMediaPlayer( + String audioId, + BufferMediaDataSource mediaDataSource, + AudiofileplayerPlugin parentAudioPlugin, + boolean looping) + throws IOException, IllegalArgumentException, IllegalStateException { + this(audioId, parentAudioPlugin, looping); + player.setDataSource(mediaDataSource); + player.prepare(); + } +} diff --git a/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/ManagedMediaPlayer.java b/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/ManagedMediaPlayer.java new file mode 100644 index 0000000..fca969b --- /dev/null +++ b/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/ManagedMediaPlayer.java @@ -0,0 +1,95 @@ +package com.google.flutter.plugins.audiofileplayer; + +import android.media.MediaPlayer; +import android.os.Handler; +import android.util.Log; + +/** Base class for wrapping a MediaPlayer for use by AudiofileplayerPlugin. */ +abstract class ManagedMediaPlayer + implements MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { + private static final String TAG = "ManagedMediaPlayer"; + protected final AudiofileplayerPlugin parentAudioPlugin; + protected final String audioId; + protected MediaPlayer player; + protected Handler handler; + + /** Runnable which repeatedly sends the player's position. */ + private final Runnable updatePositionData = + new Runnable() { + public void run() { + try { + if (player.isPlaying()) { + double positionSeconds = (double) player.getCurrentPosition() / 1000.0; + parentAudioPlugin.handlePosition(audioId, positionSeconds); + } + handler.postDelayed(this, 250); + } catch (Exception e) { + Log.e(TAG, "Could not schedule position update for player", e); + } + } + }; + + protected ManagedMediaPlayer(String audioId, AudiofileplayerPlugin parentAudioPlugin) { + this.parentAudioPlugin = parentAudioPlugin; + this.audioId = audioId; + + handler = new Handler(); + handler.post(updatePositionData); + } + + public String getAudioId() { + return audioId; + } + + public double getDurationSeconds() { + return (double) player.getDuration() / 1000.0; // Convert ms to seconds. + } + + /** Plays the audio. */ + public void play(boolean playFromStart) { + if (playFromStart) { + player.seekTo(0); + } + player.start(); + } + + /** Releases the underlying MediaPlayer. */ + public void release() { + player.stop(); + player.reset(); + player.release(); + player = null; + handler.removeCallbacks(updatePositionData); + } + + public void seek(double positionSeconds) { + int positionMilliseconds = (int) (positionSeconds * 1000.0); + player.seekTo(positionMilliseconds); + } + + public void setVolume(double volume) { + player.setVolume((float) volume, (float) volume); + } + + public void pause() { + player.pause(); + } + + @Override + public void onCompletion(MediaPlayer mediaPlayer) { + player.seekTo(0); + parentAudioPlugin.handleCompletion(this.audioId); + } + + /** + * Callback to indicate an error condition. + * + *

NOTE: {@link #onError(MediaPlayer, int, int)} must be properly implemented and return {@code + * true} otherwise errors will repeatedly call {@link #onCompletion(MediaPlayer)}. + */ + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + Log.e(TAG, "onError: what:" + what + " extra: " + extra); + return true; + } +} diff --git a/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/RemoteManagedMediaPlayer.java b/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/RemoteManagedMediaPlayer.java new file mode 100644 index 0000000..6c9d5b2 --- /dev/null +++ b/packages/audiofileplayer/android/src/main/java/com/google/flutter/plugins/audiofileplayer/RemoteManagedMediaPlayer.java @@ -0,0 +1,116 @@ +package com.google.flutter.plugins.audiofileplayer; + +import android.media.MediaPlayer; +import android.util.Log; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Wraps a MediaPlayer for remote asset use by AudiofileplayerPlugin. + * + *

Used for remote audio data only; loading occurs asynchronously, allowing program to continue + * while data is received. Callers may call all other methods on {@link ManagedMediaPlayer} + * immediately (i.e. before loading is complete); these will, if necessary, be delayed and re-called + * internally upon loading completion. + * + *

Note that with async loading, errors such as invalid URLs and lack of connectivity are + * reported asyncly via {@link RemoteManagedMediaPlayer.onError()}, instead of as Exceptions. + * Unfortunately, this yields inscrutable and/or undifferentiated error codes, instead of discrete + * Exception subclasses with human-readable error messages. + */ +class RemoteManagedMediaPlayer extends ManagedMediaPlayer + implements MediaPlayer.OnPreparedListener { + + interface OnRemoteLoadListener { + /** + * Called when asynchronous remote loading has completed, either successfully via {@link + * RemoteManagedMediaPlayer#onPrepare()}, or unsuccessfully on {@link + * RemoteManagedMediaPlayer#onError()}. + */ + void onRemoteLoadComplete(boolean success); + } + + private static final String TAG = "RemoteManagedMediaPlayer"; + private OnRemoteLoadListener onRemoteLoadListener; + private boolean isPrepared; + // A list of runnables to run once onPrepared() is called. + private List onPreparedRunnables = new ArrayList<>(); + + /** + * Create a RemoteManagedMediaPlayer from an remote URL string. + * + *

Async loading errors (during {@link MediaPlayer#prepareAsync()}) are caught by {@link + * RemoteManagedMediaPlayer#onError()}, not as Exceptions. + * + * @throws IOException if underlying MediaPlayer cannot load it as its DataSource. + */ + public RemoteManagedMediaPlayer( + String audioId, String remoteUrl, AudiofileplayerPlugin parentAudioPlugin, boolean looping) + throws IOException { + super(audioId, parentAudioPlugin); + player = new MediaPlayer(); + player.setDataSource(remoteUrl); + player.setLooping(looping); + player.setOnCompletionListener(this); + player.setOnPreparedListener(this); + player.setOnErrorListener(this); + player.prepareAsync(); + } + + public void setOnRemoteLoadListener(OnRemoteLoadListener onRemoteLoadListener) { + this.onRemoteLoadListener = onRemoteLoadListener; + } + + @Override + public void onPrepared(MediaPlayer mediaPlayer) { + Log.i(TAG, "on prepared"); + isPrepared = true; + onRemoteLoadListener.onRemoteLoadComplete(true); + for (Runnable r : onPreparedRunnables) { + r.run(); + } + } + + @Override + public void play(boolean playFromStart) { + if (!isPrepared) { + onPreparedRunnables.add(() -> RemoteManagedMediaPlayer.super.play(playFromStart)); + } else { + super.play(playFromStart); + } + } + + @Override + public void release() { + if (!isPrepared) { + onPreparedRunnables.add(() -> RemoteManagedMediaPlayer.super.release()); + } else { + super.release(); + } + } + + @Override + public void seek(double positionSeconds) { + if (!isPrepared) { + onPreparedRunnables.add(() -> RemoteManagedMediaPlayer.super.seek(positionSeconds)); + } else { + super.seek(positionSeconds); + } + } + + @Override + public void pause() { + if (!isPrepared) { + onPreparedRunnables.add(() -> RemoteManagedMediaPlayer.super.pause()); + } else { + super.pause(); + } + } + + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + onRemoteLoadListener.onRemoteLoadComplete(false); + return super.onError(mp, what, extra); + } +} diff --git a/packages/audiofileplayer/example/.gitignore b/packages/audiofileplayer/example/.gitignore new file mode 100644 index 0000000..07488ba --- /dev/null +++ b/packages/audiofileplayer/example/.gitignore @@ -0,0 +1,70 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/audiofileplayer/example/.metadata b/packages/audiofileplayer/example/.metadata new file mode 100644 index 0000000..07763f7 --- /dev/null +++ b/packages/audiofileplayer/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 8661d8aecd626f7f57ccbcb735553edc05a2e713 + channel: stable + +project_type: app diff --git a/packages/audiofileplayer/example/README.md b/packages/audiofileplayer/example/README.md new file mode 100644 index 0000000..f31a287 --- /dev/null +++ b/packages/audiofileplayer/example/README.md @@ -0,0 +1,16 @@ +# audiofileplayer_example + +Demonstrates how to use the audiofileplayer plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.io/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/audiofileplayer/example/android/app/build.gradle b/packages/audiofileplayer/example/android/app/build.gradle new file mode 100644 index 0000000..3ea061a --- /dev/null +++ b/packages/audiofileplayer/example/android/app/build.gradle @@ -0,0 +1,61 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 28 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.google.flutter.plugins.audiofileplayer_example" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} diff --git a/packages/audiofileplayer/example/android/app/src/debug/AndroidManifest.xml b/packages/audiofileplayer/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..2af7717 --- /dev/null +++ b/packages/audiofileplayer/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/audiofileplayer/example/android/app/src/main/AndroidManifest.xml b/packages/audiofileplayer/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8fc1986 --- /dev/null +++ b/packages/audiofileplayer/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/packages/audiofileplayer/example/android/app/src/main/java/com/google/flutter/plugins/audiofileplayer_example/MainActivity.java b/packages/audiofileplayer/example/android/app/src/main/java/com/google/flutter/plugins/audiofileplayer_example/MainActivity.java new file mode 100644 index 0000000..d664117 --- /dev/null +++ b/packages/audiofileplayer/example/android/app/src/main/java/com/google/flutter/plugins/audiofileplayer_example/MainActivity.java @@ -0,0 +1,13 @@ +package com.google.flutter.plugins.audiofileplayer_example; + +import android.os.Bundle; +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class MainActivity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/packages/audiofileplayer/example/android/app/src/main/res/drawable/launch_background.xml b/packages/audiofileplayer/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/packages/audiofileplayer/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/audiofileplayer/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/audiofileplayer/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/packages/audiofileplayer/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/audiofileplayer/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/audiofileplayer/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/packages/audiofileplayer/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/audiofileplayer/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/audiofileplayer/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/packages/audiofileplayer/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/audiofileplayer/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/audiofileplayer/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/packages/audiofileplayer/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/audiofileplayer/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/audiofileplayer/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/packages/audiofileplayer/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/audiofileplayer/example/android/app/src/main/res/values/styles.xml b/packages/audiofileplayer/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/packages/audiofileplayer/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/audiofileplayer/example/android/app/src/profile/AndroidManifest.xml b/packages/audiofileplayer/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..2af7717 --- /dev/null +++ b/packages/audiofileplayer/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/audiofileplayer/example/android/build.gradle b/packages/audiofileplayer/example/android/build.gradle new file mode 100644 index 0000000..bb8a303 --- /dev/null +++ b/packages/audiofileplayer/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.2.1' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/audiofileplayer/example/android/gradle.properties b/packages/audiofileplayer/example/android/gradle.properties new file mode 100644 index 0000000..8bd86f6 --- /dev/null +++ b/packages/audiofileplayer/example/android/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1536M diff --git a/packages/audiofileplayer/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/audiofileplayer/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2819f02 --- /dev/null +++ b/packages/audiofileplayer/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/audiofileplayer/example/android/settings.gradle b/packages/audiofileplayer/example/android/settings.gradle new file mode 100644 index 0000000..5a2f14f --- /dev/null +++ b/packages/audiofileplayer/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/audiofileplayer/example/assets/audio/printermanual.m4a b/packages/audiofileplayer/example/assets/audio/printermanual.m4a new file mode 100644 index 0000000..323e781 Binary files /dev/null and b/packages/audiofileplayer/example/assets/audio/printermanual.m4a differ diff --git a/packages/audiofileplayer/example/assets/audio/sinesweep.mp3 b/packages/audiofileplayer/example/assets/audio/sinesweep.mp3 new file mode 100644 index 0000000..4d8ae70 Binary files /dev/null and b/packages/audiofileplayer/example/assets/audio/sinesweep.mp3 differ diff --git a/packages/audiofileplayer/example/assets/icons/2.0x/ic_pause_black_48dp.png b/packages/audiofileplayer/example/assets/icons/2.0x/ic_pause_black_48dp.png new file mode 100755 index 0000000..792104f Binary files /dev/null and b/packages/audiofileplayer/example/assets/icons/2.0x/ic_pause_black_48dp.png differ diff --git a/packages/audiofileplayer/example/assets/icons/2.0x/ic_play_arrow_black_48dp.png b/packages/audiofileplayer/example/assets/icons/2.0x/ic_play_arrow_black_48dp.png new file mode 100755 index 0000000..d12d495 Binary files /dev/null and b/packages/audiofileplayer/example/assets/icons/2.0x/ic_play_arrow_black_48dp.png differ diff --git a/packages/audiofileplayer/example/assets/icons/ic_pause_black_48dp.png b/packages/audiofileplayer/example/assets/icons/ic_pause_black_48dp.png new file mode 100755 index 0000000..74068ea Binary files /dev/null and b/packages/audiofileplayer/example/assets/icons/ic_pause_black_48dp.png differ diff --git a/packages/audiofileplayer/example/assets/icons/ic_play_arrow_black_48dp.png b/packages/audiofileplayer/example/assets/icons/ic_play_arrow_black_48dp.png new file mode 100755 index 0000000..f208795 Binary files /dev/null and b/packages/audiofileplayer/example/assets/icons/ic_play_arrow_black_48dp.png differ diff --git a/packages/audiofileplayer/example/ios/Flutter/AppFrameworkInfo.plist b/packages/audiofileplayer/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9367d48 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/packages/audiofileplayer/example/ios/Flutter/Debug.xcconfig b/packages/audiofileplayer/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..e8efba1 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/audiofileplayer/example/ios/Flutter/Release.xcconfig b/packages/audiofileplayer/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..399e934 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/audiofileplayer/example/ios/Runner.xcodeproj/project.pbxproj b/packages/audiofileplayer/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..7bd4cce --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,563 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 67C1FF0A92EE410EB9A6ECF5 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 283EA6CAD6FBBC952F6CE58F /* libPods-Runner.a */; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 283EA6CAD6FBBC952F6CE58F /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + 67C1FF0A92EE410EB9A6ECF5 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 73E993770646A7EF8D98E434 /* Pods */ = { + isa = PBXGroup; + children = ( + ); + name = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 73E993770646A7EF8D98E434 /* Pods */, + FDE1B70AEEDF4C1E76B2A258 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + FDE1B70AEEDF4C1E76B2A258 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 283EA6CAD6FBBC952F6CE58F /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 75A978332EDCFABD9519D262 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + B8D06BAD4CB62018643DD1F4 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + }; + 75A978332EDCFABD9519D262 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + 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"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + B8D06BAD4CB62018643DD1F4 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.flutter.plugins.audiofileplayerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.flutter.plugins.audiofileplayerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.flutter.plugins.audiofileplayerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/audiofileplayer/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/audiofileplayer/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/audiofileplayer/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/audiofileplayer/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..786d6aa --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/audiofileplayer/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/audiofileplayer/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/audiofileplayer/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/audiofileplayer/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..949b678 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + BuildSystemType + Original + + diff --git a/packages/audiofileplayer/example/ios/Runner/AppDelegate.h b/packages/audiofileplayer/example/ios/Runner/AppDelegate.h new file mode 100644 index 0000000..36e21bb --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner/AppDelegate.h @@ -0,0 +1,6 @@ +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/audiofileplayer/example/ios/Runner/AppDelegate.m b/packages/audiofileplayer/example/ios/Runner/AppDelegate.m new file mode 100644 index 0000000..59a72e9 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner/AppDelegate.m @@ -0,0 +1,13 @@ +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..3d43d11 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/packages/audiofileplayer/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/audiofileplayer/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/audiofileplayer/example/ios/Runner/Base.lproj/Main.storyboard b/packages/audiofileplayer/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/audiofileplayer/example/ios/Runner/Info.plist b/packages/audiofileplayer/example/ios/Runner/Info.plist new file mode 100644 index 0000000..9c2ec66 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + audiofileplayer_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/audiofileplayer/example/ios/Runner/main.m b/packages/audiofileplayer/example/ios/Runner/main.m new file mode 100644 index 0000000..dff6597 --- /dev/null +++ b/packages/audiofileplayer/example/ios/Runner/main.m @@ -0,0 +1,9 @@ +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/audiofileplayer/example/lib/main.dart b/packages/audiofileplayer/example/lib/main.dart new file mode 100644 index 0000000..c272d83 --- /dev/null +++ b/packages/audiofileplayer/example/lib/main.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; + +import 'package:audiofileplayer/audiofileplayer.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + // Preloaded audio data for the first card. + Audio _audio; + bool _audioPlaying = false; + double _audioDurationSeconds; + double _audioPositionSeconds; + double _audioVolume = 1.0; + double _seekSliderValue = 0.0; // Normalized 0.0 - 1.0. + + // On-the-fly audio data for the second card. + int _spawnedAudioCount = 0; + + // Remote url audio data for the third card. + Audio _remoteAudio; + bool _remoteAudioPlaying = false; + bool _remoteAudioLoading = false; + String _remoteErrorMessage; + + @override + void initState() { + super.initState(); + _audio = Audio.load('assets/audio/printermanual.m4a', + onComplete: () => setState(() => _audioPlaying = false), + onDuration: (double durationSeconds) => + setState(() => _audioDurationSeconds = durationSeconds), + onPosition: (double positionSeconds) => setState(() { + _audioPositionSeconds = positionSeconds; + _seekSliderValue = _audioPositionSeconds / _audioDurationSeconds; + })); + _loadRemoteAudio(); + } + + @override + void dispose() { + _audio.dispose(); + if (_remoteAudio != null) { + _remoteAudio.dispose(); + } + super.dispose(); + } + + static Widget _transportButtonWithTitle( + String title, bool isPlaying, VoidCallback onTap) => + Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + children: [ + RaisedButton( + onPressed: onTap, + child: isPlaying + ? Image.asset("assets/icons/ic_pause_black_48dp.png") + : Image.asset( + "assets/icons/ic_play_arrow_black_48dp.png")), + Padding( + padding: EdgeInsets.symmetric(vertical: 4.0), + child: Text(title)), + ], + )); + + // convert double seconds to minutes:seconds + static String _stringForSeconds(double seconds) { + if (seconds == null) return null; + return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; + } + + void _loadRemoteAudio() { + _remoteErrorMessage = null; + _remoteAudioLoading = true; + _remoteAudio = Audio.loadFromRemoteUrl('https://streams.kqed.org/kqedradio', + onDuration: (_) => setState(() => _remoteAudioLoading = false), + onError: (String message) => setState(() { + _remoteErrorMessage = message; + _remoteAudio.dispose(); + _remoteAudio = null; + _remoteAudioPlaying = false; + _remoteAudioLoading = false; + })); + } + + // Creates a card, out of column child widgets. Injects vertical padding + // around the column children. + Widget _cardWrapper(List columnChildren) => Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: columnChildren + .map((child) => Padding( + padding: EdgeInsets.symmetric(vertical: 4.0), + child: child, + )) + .toList()))); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + backgroundColor: const Color(0xFFCCCCCC), + appBar: AppBar( + title: const Text('Audio file player example'), + ), + body: ListView(children: [ + // A card controlling a pre-loaded (on app start) audio object. + _cardWrapper([ + const Text('Preloaded audio, with transport controls.'), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + _transportButtonWithTitle('play from start', false, () { + _audio.play(); + setState(() => _audioPlaying = true); + }), + _transportButtonWithTitle( + _audioPlaying ? 'pause' : 'resume', _audioPlaying, () { + _audioPlaying ? _audio.pause() : _audio.resume(); + setState(() => _audioPlaying = !_audioPlaying); + }), + ]), + Row( + children: [ + Text(_stringForSeconds(_audioPositionSeconds) ?? ''), + Expanded(child: Container()), + Text(_stringForSeconds(_audioDurationSeconds) ?? ''), + ], + ), + Slider( + value: _seekSliderValue, + onChanged: (double val) { + setState(() => _seekSliderValue = val); + final positionSeconds = val * _audioDurationSeconds; + _audio.seek(positionSeconds); + }), + const Text('seek'), + Slider( + value: _audioVolume, + onChanged: (double val) { + setState(() => _audioVolume = val); + _audio.setVolume(_audioVolume); + }), + const Text('volume (linear amplitude)'), + ]), + _cardWrapper([ + const Text('Spawn overlapping one-shot audio playback'), + _transportButtonWithTitle('(hit multiple times)', false, () { + Audio.load('assets/audio/sinesweep.mp3', + onComplete: () => setState(() => --_spawnedAudioCount)) + ..play() + ..dispose(); + setState(() => ++_spawnedAudioCount); + }), + Text('Spawned audio count: $_spawnedAudioCount'), + ]), + _cardWrapper([ + const Text('Play remote stream'), + _transportButtonWithTitle( + 'resume/pause NPR (KQED) live stream', + _remoteAudioPlaying, + _remoteAudioLoading + ? null + : () { + if (!_remoteAudioPlaying) { + // If remote audio loading previously failed with an + // error, attempt to reload. + if (_remoteAudio == null) _loadRemoteAudio(); + // Note call to resume(), not play(). play() attempts to + // seek to the start of a file, which, for streams, will + // fail with an error on Android platforms, so streams + // should use resume() to begin playback. + _remoteAudio.resume(); + setState(() => _remoteAudioPlaying = true); + } else { + _remoteAudio.pause(); + setState(() => _remoteAudioPlaying = false); + } + }), + _remoteErrorMessage != null + ? Text(_remoteErrorMessage, + style: TextStyle(color: const Color(0xFFFF0000))) + : Text(_remoteAudioLoading ? 'loading...' : 'loaded') + ]), + ]), + )); + } +} diff --git a/packages/audiofileplayer/example/pubspec.yaml b/packages/audiofileplayer/example/pubspec.yaml new file mode 100644 index 0000000..e597c59 --- /dev/null +++ b/packages/audiofileplayer/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: audiofileplayer_example +description: Demonstrates how to use the audiofileplayer plugin. +publish_to: 'none' + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + audiofileplayer: + path: ../ + +# For information on the generic Dart part of this file, see the +# following page: https://www.dartlang.org/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + uses-material-design: true + + assets: + - assets/audio/ + - assets/icons/ + diff --git a/packages/audiofileplayer/example/test/widget_test.dart b/packages/audiofileplayer/example/test/widget_test.dart new file mode 100644 index 0000000..5627c53 --- /dev/null +++ b/packages/audiofileplayer/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:audiofileplayer_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/packages/audiofileplayer/ios/.gitignore b/packages/audiofileplayer/ios/.gitignore new file mode 100644 index 0000000..710ec6c --- /dev/null +++ b/packages/audiofileplayer/ios/.gitignore @@ -0,0 +1,36 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig diff --git a/packages/audiofileplayer/ios/Assets/.gitkeep b/packages/audiofileplayer/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/audiofileplayer/ios/Classes/AudiofileplayerPlugin.h b/packages/audiofileplayer/ios/Classes/AudiofileplayerPlugin.h new file mode 100644 index 0000000..9ed475c --- /dev/null +++ b/packages/audiofileplayer/ios/Classes/AudiofileplayerPlugin.h @@ -0,0 +1,10 @@ +#import + +/** + * Flutter audio file player plugin. + * + * Receives messages which create, trigger, and destroy instances of + * FLTManagedPlayer. + */ +@interface AudiofileplayerPlugin : NSObject +@end diff --git a/packages/audiofileplayer/ios/Classes/AudiofileplayerPlugin.m b/packages/audiofileplayer/ios/Classes/AudiofileplayerPlugin.m new file mode 100644 index 0000000..483298e --- /dev/null +++ b/packages/audiofileplayer/ios/Classes/AudiofileplayerPlugin.m @@ -0,0 +1,211 @@ +#import "AudiofileplayerPlugin.h" +#import "ManagedPlayer.h" + +static NSString *const kChannel = @"audiofileplayer"; +static NSString *const kLoadMethod = @"load"; +static NSString *const kFlutterPath = @"flutterPath"; +static NSString *const kAudioBytes = @"audioBytes"; +static NSString *const kRemoteUrl = @"remoteUrl"; +static NSString *const kAudioId = @"audioId"; +static NSString *const kLooping = @"looping"; +static NSString *const kReleaseMethod = @"release"; +static NSString *const kPlayMethod = @"play"; +static NSString *const kPlayFromStart = @"playFromStart"; +static NSString *const kSeekMethod = @"seek"; +static NSString *const kSetVolumeMethod = @"setVolume"; +static NSString *const kVolume = @"volume"; +static NSString *const kPauseMethod = @"pause"; +static NSString *const kOnCompleteCallback = @"onComplete"; +static NSString *const kOnDurationCallback = @"onDuration"; +static NSString *const kDurationSeconds = @"duration_seconds"; +static NSString *const kOnPositionCallback = @"onPosition"; +static NSString *const kPositionSeconds = @"position_seconds"; +static NSString *const kErrorCode = @"AudioPluginError"; + +@interface AudiofileplayerPlugin () +@end + +@implementation AudiofileplayerPlugin { + NSObject *_registrar; + FlutterMethodChannel *_channel; + NSMutableDictionary *_playersDict; +} + ++ (void)registerWithRegistrar:(NSObject*)registrar { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:kChannel + binaryMessenger:[registrar messenger]]; + AudiofileplayerPlugin* instance = + [[AudiofileplayerPlugin alloc] initWithRegistrar:registrar channel:channel]; + [registrar addMethodCallDelegate:instance channel:channel]; + [registrar addApplicationDelegate:instance]; +} + +- (instancetype)initWithRegistrar:(NSObject *)registrar + channel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _registrar = registrar; + _channel = channel; + _playersDict = [NSMutableDictionary dictionary]; + } + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + NSLog(@"handleMethodCall: method = %@", call.method); + if ([call.method isEqualToString:@"load"]) { + [self handleLoadWithCall:call result:result]; + return; + } + + // All subsequent calls need a valid player. + NSString *audioId = call.arguments[@"audioId"]; + if (!audioId) { + result([FlutterError + errorWithCode:kErrorCode + message:[NSString + stringWithFormat:@"Received %@ call without an audioId", call.method] + details:nil]); + return; + } + FLTManagedPlayer *player = _playersDict[audioId]; + if (!player) { + result([FlutterError + errorWithCode:kErrorCode + message:[NSString stringWithFormat:@"Called %@ on an unloaded player: %@", + call.method, audioId] + details:nil]); + return; + } + + if ([call.method isEqualToString:kPlayMethod]) { + bool playFromStart = [call.arguments[kPlayFromStart] boolValue]; + [player play:playFromStart]; + result(nil); + } else if ([call.method isEqualToString:kReleaseMethod]) { + [player releasePlayer]; + [_playersDict removeObjectForKey:audioId]; + result(nil); + } else if ([call.method isEqualToString:kSeekMethod]) { + NSTimeInterval position = [call.arguments[kPositionSeconds] doubleValue]; + [player seek:position]; + result(nil); + } else if ([call.method isEqualToString:kSetVolumeMethod]) { + double volume = [call.arguments[kVolume] doubleValue]; + [player setVolume:volume]; + result(nil); + } else if ([call.method isEqualToString:kPauseMethod]) { + [player pause]; + result(nil); + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)handleLoadWithCall:(FlutterMethodCall *)call result:(FlutterResult)result { + NSString *audioId = call.arguments[kAudioId]; + if (!audioId) { + result([FlutterError errorWithCode:kErrorCode + message:@"Received load call without an audioId" + details:nil]); + return; + } + if (_playersDict[audioId]) { + result([FlutterError + errorWithCode:kErrorCode + message:[NSString + stringWithFormat:@"Tried to load an already-loaded player: %@", audioId] + details:nil]); + return; + } + + bool isLooping = [call.arguments[kLooping] boolValue]; + + FLTManagedPlayer *player = nil; + if (call.arguments[kFlutterPath] != [NSNull null]) { + NSString *flutterPath = call.arguments[kFlutterPath]; + NSString *key = [_registrar lookupKeyForAsset:flutterPath]; + NSString *path = [[NSBundle mainBundle] pathForResource:key ofType:nil]; + if (!path) { + result([FlutterError + errorWithCode:kErrorCode + message:[NSString stringWithFormat: + @"Could not get path for flutter asset %@ for audio %@ ", + flutterPath, audioId] + details:nil]); + return; + } + player = [[FLTManagedPlayer alloc] initWithAudioId:audioId + path:path + delegate:self + isLooping:isLooping]; + _playersDict[audioId] = player; + result(nil); + } else if (call.arguments[kAudioBytes] != [NSNull null]) { + FlutterStandardTypedData *flutterData = call.arguments[kAudioBytes]; + player = [[FLTManagedPlayer alloc] initWithAudioId:audioId + data:[flutterData data] + delegate:self + isLooping:isLooping]; + _playersDict[audioId] = player; + result(nil); + } else if (call.arguments[kRemoteUrl] != [NSNull null]) { + NSString *urlString = call.arguments[kRemoteUrl]; + // Load player, but wait for remote loading to succeed/fail before returning the methodCall. + __weak AudiofileplayerPlugin *weakSelf = self; + player = [[FLTManagedPlayer alloc] + initWithAudioId:audioId + remoteUrl:urlString + delegate:self + isLooping:isLooping + remoteLoadHandler:^(BOOL success) { + if (success) { + result(nil); + } else { + AudiofileplayerPlugin *strongSelf = weakSelf; + if (strongSelf) { + [strongSelf->_playersDict removeObjectForKey:audioId]; + } + result([FlutterError + errorWithCode:kErrorCode + message:[NSString + stringWithFormat:@"Could not load remote URL %@ for player %@", + urlString, audioId] + details:nil]); + } + }]; + // Put AVPlayer into dictionary syncl'y on creation. Will be removed in the remoteLoadHandler + // if remote loading fails. + _playersDict[audioId] = player; + } else { + result([FlutterError errorWithCode:kErrorCode + message:@"Could not create ManagedMediaPlayer with neither " + @"flutterPath nor audioBytes nor remoteUrl" + details:nil]); + } +} + +#pragma mark - FLTManagedPlayerDelegate + +- (void)managedPlayerDidFinishPlaying:(NSString *)audioId { + [_channel invokeMethod:kOnCompleteCallback arguments:@{kAudioId : audioId}]; +} + +- (void)managedPlayerDidUpdatePosition:(NSTimeInterval)position forAudioId:(NSString *)audioId { + [_channel invokeMethod:kOnPositionCallback + arguments:@{ + kAudioId : audioId, + kPositionSeconds : @(position), + }]; +} + +- (void)managedPlayerDidLoadWithDuration:(NSTimeInterval)duration forAudioId:(NSString *)audioId { + [_channel invokeMethod:kOnDurationCallback + arguments:@{ + kAudioId : audioId, + kDurationSeconds : @(duration), + }]; +} + +@end diff --git a/packages/audiofileplayer/ios/Classes/ManagedPlayer.h b/packages/audiofileplayer/ios/Classes/ManagedPlayer.h new file mode 100644 index 0000000..b156214 --- /dev/null +++ b/packages/audiofileplayer/ios/Classes/ManagedPlayer.h @@ -0,0 +1,50 @@ +#import + +@class FLTManagedPlayer; + +@protocol FLTManagedPlayerDelegate + +/** + * Called by FLTManagedPlayer when a non-looping sound has finished playback, + * or on calling stop(). + */ +- (void)managedPlayerDidFinishPlaying:(NSString *)audioId; + +/** Called by FLTManagedPlayer repeatedly while audio is playing. */ +- (void)managedPlayerDidUpdatePosition:(NSTimeInterval)position forAudioId:(NSString *)audioId; + +/** Called by FLTManagedPlayer when media is loaded and duration is known. */ +- (void)managedPlayerDidLoadWithDuration:(NSTimeInterval)duration forAudioId:(NSString *)audioId; + +@end + +/** Wraps an AVAudioPlayer or AVPlayer for use by AudiofileplayerPlugin. */ +@interface FLTManagedPlayer : NSObject + +@property(nonatomic, readonly) NSString *audioId; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithAudioId:(NSString *)audioId + path:(NSString *)path + delegate:(id)delegate + isLooping:(bool)isLooping; + +- (instancetype)initWithAudioId:(NSString *)audioId + data:(NSData *)data + delegate:(id)delegate + isLooping:(bool)isLooping; + +- (instancetype)initWithAudioId:(NSString *)audioId + remoteUrl:(NSString *)urlString + delegate:(id)delegate + isLooping:(bool)isLooping + remoteLoadHandler:(void (^)(BOOL))remoteLoadHandler; + +- (void)play:(bool)playFromStart; +- (void)releasePlayer; +- (void)seek:(NSTimeInterval)position; +- (void)setVolume:(double)volume; +- (void)pause; + +@end diff --git a/packages/audiofileplayer/ios/Classes/ManagedPlayer.m b/packages/audiofileplayer/ios/Classes/ManagedPlayer.m new file mode 100644 index 0000000..86d7ffc --- /dev/null +++ b/packages/audiofileplayer/ios/Classes/ManagedPlayer.m @@ -0,0 +1,232 @@ +#import "ManagedPlayer.h" + +#import + +static NSString *const kKeyPathStatus = @"status"; +static float const kTimerUpdateIntervalSeconds = 0.25; + +@interface FLTManagedPlayer () +@end + +@implementation FLTManagedPlayer { + __weak id _delegate; + AVAudioPlayer *_audioPlayer; + NSTimer *_positionTimer; + + AVPlayer *_avPlayer; + id _completionObserver; // Registered on NSNotificationCenter. + id _timeObserver; // Registered on the AVPlayer. + void (^_remoteLoadHandler)(BOOL); // Called on AVPlayer loading status change observed. +} + +// Private common initializer. [audioPlayer] or [avPlayer], but not both, must be set. +- (instancetype)initWithAudioId:(NSString *)audioId + audioPlayer:(AVAudioPlayer *)audioPlayer + avPlayer:(AVPlayer *)avPlayer + delegate:(id)delegate + isLooping:(bool)isLooping + remoteLoadHandler:(void (^)(BOOL))remoteLoadHandler { + // Assert init with either AVAudioPlayer or AVPlayer. + if ((audioPlayer == nil) == (avPlayer == nil)) { + NSLog(@"Must initialize with either audioPlayer or avPlayer"); + return nil; + } + self = [super init]; + if (self) { + _audioId = [audioId copy]; + _delegate = delegate; + if (audioPlayer) { + _audioPlayer = audioPlayer; + _audioPlayer.delegate = self; + _audioPlayer.numberOfLoops = isLooping ? -1 : 0; + [_audioPlayer prepareToPlay]; + [_delegate managedPlayerDidLoadWithDuration:_audioPlayer.duration forAudioId:_audioId]; + _positionTimer = [NSTimer + scheduledTimerWithTimeInterval:kTimerUpdateIntervalSeconds + repeats:YES + block:^(NSTimer *timer) { + if (_audioPlayer.playing) { + [_delegate + managedPlayerDidUpdatePosition:_audioPlayer.currentTime + forAudioId:_audioId]; + } + }]; + } else { + _avPlayer = avPlayer; + _remoteLoadHandler = remoteLoadHandler; + CMTime interval = CMTimeMakeWithSeconds(kTimerUpdateIntervalSeconds, NSEC_PER_SEC); + FLTManagedPlayer *__weak weakSelf = self; + _timeObserver = [_avPlayer + addPeriodicTimeObserverForInterval:interval + queue:nil + usingBlock:^(CMTime time) { + FLTManagedPlayer *strongSelf = weakSelf; + if (strongSelf) { + NSTimeInterval position = + (NSTimeInterval)CMTimeGetSeconds(time); + [strongSelf->_delegate + managedPlayerDidUpdatePosition:position + forAudioId:strongSelf->_audioId]; + } + }]; + _completionObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:AVPlayerItemDidPlayToEndTimeNotification + object:_avPlayer.currentItem + queue:nil + usingBlock:^(NSNotification *notif) { + [_avPlayer seekToTime:kCMTimeZero]; + [_delegate managedPlayerDidFinishPlaying:_audioId]; + }]; + [_avPlayer.currentItem addObserver:self + forKeyPath:kKeyPathStatus + options:NSKeyValueObservingOptionNew + context:nil]; + } + } + return self; +} + +- (instancetype)initWithAudioId:(NSString *)audioId + path:(NSString *)path + delegate:(id)delegate + isLooping:(bool)isLooping { + AVAudioPlayer *audioPlayer = + [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path] error:nil]; + return [self initWithAudioId:audioId + audioPlayer:audioPlayer + avPlayer:nil + delegate:delegate + isLooping:isLooping + remoteLoadHandler:nil]; +} + +- (instancetype)initWithAudioId:(NSString *)audioId + data:(NSData *)data + delegate:(id)delegate + isLooping:(bool)isLooping { + AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithData:data error:nil]; + return [self initWithAudioId:audioId + audioPlayer:audioPlayer + avPlayer:nil + delegate:delegate + isLooping:isLooping + remoteLoadHandler:nil]; +} + +- (instancetype)initWithAudioId:(NSString *)audioId + remoteUrl:(NSString *)urlString + delegate:(id)delegate + isLooping:(bool)isLooping + remoteLoadHandler:(void (^)(BOOL))remoteLoadHandler { + AVPlayerItem *avPlayerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:urlString]]; + AVPlayer *avPlayer = [[AVPlayer alloc] initWithPlayerItem:avPlayerItem]; + return [self initWithAudioId:audioId + audioPlayer:nil + avPlayer:avPlayer + delegate:delegate + isLooping:isLooping + remoteLoadHandler:remoteLoadHandler]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:_completionObserver]; + [_avPlayer.currentItem removeObserver:self forKeyPath:kKeyPathStatus]; + [_avPlayer removeTimeObserver:_timeObserver]; +} + +- (void)play:(bool)playFromStart { + if (_audioPlayer) { + if (playFromStart) { + _audioPlayer.currentTime = 0; + } + [_audioPlayer play]; + } else { + if (playFromStart) { + [_avPlayer seekToTime:kCMTimeZero]; + } + [_avPlayer play]; + } +} + +- (void)releasePlayer { + if (_audioPlayer) { + [_audioPlayer stop]; // Undoes the resource aquisition in [prepareToPlay]. + [_positionTimer invalidate]; + _positionTimer = nil; + } else { + [_avPlayer pause]; + [_avPlayer.currentItem removeObserver:self forKeyPath:kKeyPathStatus]; + [_avPlayer removeTimeObserver:_timeObserver]; + _avPlayer = nil; + } +} + +- (void)seek:(NSTimeInterval)position { + if (_audioPlayer) { + _audioPlayer.currentTime = position; + } else { + [_avPlayer seekToTime:CMTimeMakeWithSeconds(position, NSEC_PER_SEC)]; + } +} + +- (void)setVolume:(double)volume { + if (_audioPlayer) { + _audioPlayer.volume = volume; + } else { + _avPlayer.volume = volume; + } +} + +- (void)pause { + if (_audioPlayer) { + [_audioPlayer pause]; + } else { + [_avPlayer pause]; + } +} + +#pragma mark - KVO + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([keyPath isEqualToString:kKeyPathStatus] && [object isKindOfClass:[AVPlayerItem class]]) { + AVPlayerItem *item = (AVPlayerItem *)object; + AVPlayerItemStatus status = [change[NSKeyValueChangeNewKey] integerValue]; + switch (status) { + case AVPlayerItemStatusReadyToPlay: { + NSTimeInterval duration = (NSTimeInterval)CMTimeGetSeconds(item.duration); + _remoteLoadHandler(YES); + [_delegate managedPlayerDidLoadWithDuration:duration forAudioId:_audioId]; + break; + } + case AVPlayerItemStatusFailed: { + if (item.error.code == -11800) { + NSLog(@"It looks like you are failing to load a remote asset. You probably requested a " + @"non-http url, and didn't specify arbitrary url loading in your app transport " + @"securty settings. Add an NSAppTransportSecurity entry to your Info.plist."); + } + _remoteLoadHandler(NO); + break; + } + case AVPlayerItemStatusUnknown: + _remoteLoadHandler(NO); + break; + } + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +#pragma mark - AVAudioPlayerDelegate + +- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)audioPlayer successfully:(BOOL)flag { + _audioPlayer.currentTime = 0; + [_delegate managedPlayerDidFinishPlaying:_audioId]; +} + +- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)audioPlayer error:(NSError *)error { +} + +@end diff --git a/packages/audiofileplayer/ios/audiofileplayer.podspec b/packages/audiofileplayer/ios/audiofileplayer.podspec new file mode 100644 index 0000000..7bc88f3 --- /dev/null +++ b/packages/audiofileplayer/ios/audiofileplayer.podspec @@ -0,0 +1,21 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'audiofileplayer' + s.version = '0.0.1' + s.summary = 'A Flutter plugin for audio playback.' + s.description = <<-DESC +A Flutter plugin for audio playback. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.ios.deployment_target = '8.0' + s.platform = :ios +end + diff --git a/packages/audiofileplayer/lib/audiofileplayer.dart b/packages/audiofileplayer/lib/audiofileplayer.dart new file mode 100644 index 0000000..dbdb1ec --- /dev/null +++ b/packages/audiofileplayer/lib/audiofileplayer.dart @@ -0,0 +1,583 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' show AppLifecycleState; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:uuid/uuid.dart'; + +final _logger = Logger('audio'); + +@visibleForTesting +const channelName = 'audiofileplayer'; +const loadMethod = 'load'; +const flutterPathKey = 'flutterPath'; +const audioBytesKey = 'audioBytes'; +const remoteUrlKey = 'remoteUrl'; +const audioIdKey = 'audioId'; +const loopingKey = 'looping'; +const releaseMethod = 'release'; +const playMethod = 'play'; +const playFromStartKey = 'playFromStart'; +const seekMethod = 'seek'; +const setVolumeMethod = 'setVolume'; +const volumeKey = 'volume'; +const pauseMethod = 'pause'; +const onCompleteCallback = 'onComplete'; +const onDurationCallback = 'onDuration'; +const durationSecondsKey = 'duration_seconds'; +const onPositionCallback = 'onPosition'; +const positionSecondsKey = 'position_seconds'; +const errorCode = 'AudioPluginError'; + +/// A plugin for audio playback. +/// +/// Example usage: +/// ```dart +/// // Play a sound as a one-shot. +/// Audio.load('assets/foo.wav')..play()..dispose(); +/// +/// // Play a sound, with the ability to play it again, or stop it later. +/// var audio = Audio.load('assets/foo.wav')..play(); +/// // ... +/// audio.play(); +/// // ... +/// audio.pause(); +/// // ... +/// audio.resume(); +/// // ... +/// audio.pause(); +/// audio.dispose(); +/// +/// // Do something when playback finishes. +/// var audio = Audio.load('assets/foo.wav', onComplete = () { ... }); +/// audio.play(); +/// // Note that after calling [dispose], audio playback will continue, and +/// // onComplete will still be run. +/// audio.dispose(); +/// ``` +/// +/// A note on sync and async usage. +/// The async methods below ([play], [pause], etc) may be used sync'ly or +/// async'ly. If used with 'await', they return when the native layer has +/// finished executing the desired operation. This may be useful for +/// synchronizing aural and visual elements. For example: +/// ```dart +/// final audio = Audio.load('foo.wav'); +/// audio.play(); +/// animationController.forward() +/// ``` +/// would send messages to the native layer to begin loading and playback, but +/// return (and start the animation) immediately. Wheras: +/// ```dart +/// final audio = Audio.load('foo.wav'); +/// await audio.play(); +/// animationController.forward() +/// ``` +/// would wait until the native command to play returned, and the audio actually +/// began playing. The animation is therefore more closely synchronized with +/// the start of audio playback. +/// +/// A note on Audio lifecycle. +/// Audio objects, and the underlying native audio resources, have different +/// lifespans, depending on usage. +/// Instances of Audio may be kept alive after calls to [dispose] (and, if they +/// are ivars, after their containing parents are dealloc'ed), so that they may +/// continue to receive and handle callbacks (e.g. onComplete). +/// The underlying native audio classes may be kept alive after [dispose] and +/// the deallocation of the parent Audio object, so that audio may continue to +/// play. +/// This command would let the Audio instance deallocate immediately; the +/// underlying resources are released once the file finishes playing. +/// ```dart +/// Audio.load('x.wav')..play()..dispose(); +/// ``` +/// This command would keep the Audio instance alive for the duration of the +/// playback (so that its onComplete can be called); the underlying resources +/// are released once the file finishes playing. +/// ```dart +/// Audio.load('x.wav', looping:true, onComplete:()=>{...})..play()..dispose(); +/// ``` +/// ```dart +/// This command would let the Audio instance deallocate immediately; underlying +/// native resources keep playback going forever. +/// ```dart +/// Audio.load('x.wav', looping:true)..play()..dispose(); +/// ``` +/// While this command would keep the Audio instance alive forever (waiting to +/// run [onComplete]), and also keep native resources playing forever: +/// ```dart +/// Audio.load('x.wav', looping:true, onComplete:()=>{...})..play()..dispose(); +/// ``` +/// +/// Usage with State objects. +/// If your Audio object is an ivar in a State object and it uses a callback +/// (e.g. onComplete), then that callback keeps a strong reference to the +/// surrounding class. Since Audio instances can outlast their parent State +/// objects, that means that the parent State object may be kept alive +/// unneccessarily, unless that strong reference is broken. +/// In State.dispose(), do either: +/// A) If you want the audio to stop playback on parent State disposal, call +/// pause(): +///```dart +/// void dispose() { +/// _audio.pause(); +/// _audio.dispose(); +/// super.dispose(); +/// } +///``` +/// or +/// B) If you want audio to continue playing back after parent State disposal, +/// call [removeCallbacks]: +///```dart +/// void dispose() { +/// _audio.removeCallbacks(); +/// _audio.dispose(); +/// super.dispose(); +/// } +///``` +/// In both cases, [pause] or [removeCallbacks] will signal that the Audio +/// instance need not stay alive (after [dispose]) to call its callbacks. +/// The Audio instance and the parent State will be dealloced. +class Audio with WidgetsBindingObserver { + @visibleForTesting + static final channel = MethodChannel(channelName) + ..setMethodCallHandler(handleMethodCall); + + static final _uuid = Uuid(); + + // All extant, undisposed Audio objects. + static final _undisposedAudios = Map(); + @visibleForTesting + static int get undisposedAudiosCount => _undisposedAudios.length; + + // All Audio objects (including disposed ones), that are playing and have an + // onComplete callback. + static final _awaitingOnCompleteAudios = Map(); + @visibleForTesting + static int get awaitingOnCompleteAudiosCount => + _awaitingOnCompleteAudios.length; + + // All Audio objects (including disposed ones), that are awaiting an + // onDuration callback. + static final _awaitingOnDurationAudios = Map(); + @visibleForTesting + static int get awaitingOnDurationAudiosCount => + _awaitingOnDurationAudios.length; + + // All Audio objects (including disposed ones), that are using an onPosition + // callback. Audios are added on play()/resume() and removed on + // pause()/playback completion. + static final _usingOnPositionAudios = Map(); + @visibleForTesting + static int get usingOnPositionAudiosCount => _usingOnPositionAudios.length; + + // All Audio objects (including disposed ones), that are using an onError + // callback. + static final _usingOnErrorAudios = Map(); + + /// Whether audio should continue playing while app is paused (i.e. + /// backgrounded). May be set at any time while the app is active, but only + /// has an effect when app is paused. + static var shouldPlayWhileAppPaused = false; + + final String _path; + final Uint8List _audioBytes; + final String _remoteUrl; + final String _audioId; + + void Function() _onComplete; + void Function(double duration) _onDuration; + void Function(double position) _onPosition; + void Function(String message) _onError; + + bool _looping; + bool _playing = false; + double _volume = 1.0; + bool _appPaused = false; + + Audio._path(this._path, this._onComplete, this._onDuration, this._onPosition, + this._onError, this._looping) + : _audioId = _uuid.v4(), + _audioBytes = null, + _remoteUrl = null { + WidgetsBinding.instance.addObserver(this); + } + + Audio._byteData(ByteData byteData, this._onComplete, this._onDuration, + this._onPosition, this._onError, this._looping) + : _audioId = _uuid.v4(), + _audioBytes = Uint8List.view(byteData.buffer), + _path = null, + _remoteUrl = null { + WidgetsBinding.instance.addObserver(this); + } + + Audio._remoteUrl(this._remoteUrl, this._onComplete, this._onDuration, + this._onPosition, this._onError, this._looping) + : _audioId = _uuid.v4(), + _audioBytes = null, + _path = null { + WidgetsBinding.instance.addObserver(this); + } + + /// Creates an Audio from an asset. + /// + /// Returns null if asset cannot be loaded. + /// Note that it returns an Audio sync'ly, though loading occurs async'ly. + static Audio load(String path, + {void onComplete(), + void onDuration(double duration), + void onPosition(double position), + void onError(String message), + bool looping = false}) { + final audio = + Audio._path(path, onComplete, onDuration, onPosition, onError, looping) + .._load(); + return audio; + } + + /// Creates an Audio from a ByteData. + /// + /// Returns null if asset cannot be loaded. + /// Note that it returns an Audio sync'ly, though loading occurs async'ly. + static Audio loadFromByteData(ByteData byteData, + {void onComplete(), + void onDuration(double duration), + void onPosition(double position), + void onError(String message), + bool looping = false}) { + final audio = Audio._byteData( + byteData, onComplete, onDuration, onPosition, onError, looping) + .._load(); + return audio; + } + + /// Creates an Audio from a remote URL. + /// + /// Returns null if url is invalid. + /// Note that it returns an Audio sync'ly, though loading occurs async'ly. + /// Note that onError will fire if remote loading fails (due to connectivity, + /// invalid url, etc); this usually is fairly quick on iOS, but waits for + /// a longer timeout on Android. + static Audio loadFromRemoteUrl(String url, + {void onComplete(), + void onDuration(double duration), + void onPosition(double position), + void onError(String message), + bool looping = false}) { + if (Uri.tryParse(url) == null) return null; + final audio = Audio._remoteUrl( + url, onComplete, onDuration, onPosition, onError, looping) + .._load(); + return audio; + } + + /// Loads an asset. + /// + /// Keeps strong reference to this Audio (for channel callback routing) + /// and requests underlying resource loading. + Future _load() async { + assert(_path != null || _audioBytes != null || _remoteUrl != null); + assert(!_undisposedAudios.containsKey(_audioId)); + _logger.info('Loading audio ${_audioId}'); + // Note that we add the _audioId to _undisposedAudios before invoking a + // load, anticipating success, so that _load() may be called async'ly, with + // a subsequent call to play(). + _undisposedAudios[_audioId] = this; + if (_onDuration != null) _awaitingOnDurationAudios[_audioId] = this; + if (_onError != null) _usingOnErrorAudios[_audioId] = this; + + try { + await _sendMethodCall(_audioId, loadMethod, { + flutterPathKey: _path, + audioBytesKey: _audioBytes, + remoteUrlKey: _remoteUrl, + audioIdKey: _audioId, + loopingKey: _looping + }); + } on PlatformException catch (_) { + // Note that exceptions during [_load] are assumed to have failed to + // create underlying resources, so a call to [_releaseNative] is not + // required. Just remove the instance from the static structures it was + // added to within this call to [_load]. + _undisposedAudios.remove(_audioId); + _awaitingOnDurationAudios.remove(_audioId); + // If this Audio does not use an onError callback, rethrow the exception. + if (_usingOnErrorAudios.remove(_audioId) == null) rethrow; + } + } + + /// Dispose this Audio. + /// + /// This must be called before object falls out of its local scope, or else + /// a memory leak may occur. Once [dispose] is called, no further calls to + /// the Audio object are accepted. + /// Triggers release of the underlying audio resources, either immediately, + /// or on playback completion. + /// Note that on calling [dispose], audio playback will continue, and + /// onComplete will still be called on playback completion. + Future dispose() async { + if (!_undisposedAudios.containsKey(_audioId)) { + _logger.severe('Called dispose() on a disposed Audio'); + return; + } + _undisposedAudios.remove(_audioId); + + // If not playing, call for release immediately. Otherwise (if audio is + // playing) it will be called when playback completes. + if (!_playing) { + _usingOnErrorAudios.remove(_audioId); + WidgetsBinding.instance.removeObserver(this); + await _releaseNative(_audioId); + } + } + + /// Remove callbacks from this Audio. + /// + /// This is useful when audio playback outlasts the lifespan of its parent + /// object. If the Audio object has callbacks which retain the surrounding + /// parent object, it will keep that parent object alive until it is finished + /// playback (even after a call to [dispose]). This may not be desired, + /// particularly if the callbacks are used just to update the parent object + /// (i.e. they call setState() on a State object). Calling removeCallbacks on + /// the parent object (e.g. in a State.dispose()) will break that reference. + void removeCallbacks() { + _onComplete = null; + _awaitingOnCompleteAudios.remove(_audioId); + _onDuration = null; + _awaitingOnDurationAudios.remove(_audioId); + _onPosition = null; + _usingOnPositionAudios.remove(_audioId); + } + + /// Plays this [Audio] content from the beginning. + /// + /// Note that remote audio streams should not call this to begin playback; + /// call [resume] instead. Android systems will throw an error if attempting + /// to seek to the start of a remote audio stream. + Future play() async { + if (!_undisposedAudios.containsKey(_audioId)) { + _logger.severe('Called play() on a disposed Audio'); + return; + } + await _playHelper(playFromStart: true); + } + + /// Resumes audio playback from the current playback position. + /// + /// Note that on a freshly-loaded Audio (at playback position zero), this is + /// equivalent to calling [play]. + Future resume() async { + if (!_undisposedAudios.containsKey(_audioId)) { + _logger.severe('Called resume() on a disposed Audio'); + return; + } + await _playHelper(playFromStart: false); + } + + // Shared code for both [play] and [resume]. + Future _playHelper({@required bool playFromStart}) async { + _playing = true; + + if (_onComplete != null) { + // If there is an onComplete, put [this] into a data structure to keep + // it alive until playback completes and onComplete can be run. + _awaitingOnCompleteAudios[_audioId] = this; + } + if (_onPosition != null) { + _usingOnPositionAudios[_audioId] = this; + } + + // If app is paused and audio should not play, return early. On app resume, + // the _playing flag will signify that audio should resume. + if (_appPaused && !shouldPlayWhileAppPaused) return; + + await _playNative(playFromStart); + } + + /// Pauses playing audio. + Future pause() async { + if (!_undisposedAudios.containsKey(_audioId)) { + _logger.severe('Called pause() on a disposed Audio'); + return; + } + + _playing = false; + _usingOnPositionAudios.remove(_audioId); + + // If audio is in [_awaitingOnCompleteAudios], remove it, without calling + // its _onComplete(); + _awaitingOnCompleteAudios.remove(_audioId); + + await _pauseNative(); + } + + /// Seeks to a playback position. + /// + /// May be used while either playing or paused. + Future seek(double positionSeconds) async { + if (!_undisposedAudios.containsKey(_audioId)) { + _logger.severe('Called seek() on a disposed Audio'); + return; + } + + try { + await _sendMethodCall(_audioId, seekMethod, + {audioIdKey: _audioId, positionSecondsKey: positionSeconds}); + } on PlatformException catch (_) { + // If this Audio does not use an onError callback, rethrow the exception. + if (!_usingOnErrorAudios.containsKey(_audioId)) rethrow; + } + } + + /// Gets/Sets volume. + /// Note that this is a linear amplitude multiplier; callers should use a sqrt + /// value of 0-1 to get an equal-power fade, e.g. 'half volume' should + /// be audio.setVolume(sqrt(.5)), to get something that 'sounds' half as + /// loud as 'full' volume. + double get volume => _volume; + + Future setVolume(double volume) async { + if (!_undisposedAudios.containsKey(_audioId)) { + _logger.severe('Called set volume on a disposed Audio'); + return; + } + if (volume < 0.0 || volume > 1.0) { + _logger.warning( + 'Invalid volume value $volume is begin clamped to 0.0 to 1.0.'); + volume.clamp(0.0, 1.0); + } + + _volume = volume; + + try { + await _sendMethodCall( + _audioId, setVolumeMethod, {audioIdKey: _audioId, volumeKey: volume}); + } on PlatformException catch (_) { + // If this Audio does not use an onError callback, rethrow the exception. + if (!_usingOnErrorAudios.containsKey(_audioId)) rethrow; + } + } + + /// Handle audio lifecycle changes. + didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused) { + _appPaused = true; + if (_playing && !shouldPlayWhileAppPaused) { + _pauseNative(); + } + } else if (state == AppLifecycleState.resumed) { + _appPaused = false; + if (_playing && !shouldPlayWhileAppPaused) { + _playNative(false); + } + } + } + + /// Sends method call for starting playback. + _playNative(bool playFromStart) async { + try { + await _sendMethodCall(_audioId, playMethod, + {audioIdKey: _audioId, playFromStartKey: playFromStart}); + } on PlatformException catch (_) { + // If this Audio does not use an onError callback, rethrow the exception. + if (!_usingOnErrorAudios.containsKey(_audioId)) rethrow; + } + } + + /// Sends method call for pausing playback. + _pauseNative() async { + try { + await _sendMethodCall(_audioId, pauseMethod, {audioIdKey: _audioId}); + } on PlatformException catch (_) { + // If this Audio does not use an onError callback, rethrow the exception. + if (!_usingOnErrorAudios.containsKey(_audioId)) rethrow; + } + } + + /// Handles callback from native layer, signifying that an [Audio] has + /// completed playback. + static void _onCompleteNative(String audioId) { + final audio = _undisposedAudios[audioId]; + if (audio != null) { + audio._playing = false; + } else { + // The audio has been disposed, so release native resources. + _usingOnErrorAudios.remove(audioId); + WidgetsBinding.instance.removeObserver(audio); + _releaseNative(audioId); + } + + // If audio is in [_awaitingOnCompleteAudios], remove it and call its + // _onComplete(); + _awaitingOnCompleteAudios.remove(audioId)?._onComplete(); + // If audio is in [_usingOnPositionAudios], remove it. + _usingOnPositionAudios.remove(audioId); + } + + /// Handles callback from native layer, signifying that a newly loaded Audio + /// has computed its duration. + static void _onDurationNative(String audioId, double durationSeconds) { + // If audio is in [_awaitingOnDurationAudios], remove it and call its + // _onDuration. + _awaitingOnDurationAudios.remove(audioId)?._onDuration(durationSeconds); + } + + /// Handles callback from native layer, signifying playback position updates. + static void _onPositionNative(String audioId, double positionSeconds) { + _usingOnPositionAudios[audioId]?._onPosition(positionSeconds); + } + + /// Release underlying audio assets. + static Future _releaseNative(String audioId) async { + try { + await _sendMethodCall(audioId, releaseMethod, {audioIdKey: audioId}); + } on PlatformException catch (_) { + // If this Audio does not use an onError callback, rethrow the exception. + if (!_usingOnErrorAudios.containsKey(audioId)) rethrow; + } + } + + // Subsequent methods interact directly with native layers. + + /// Call channel.invokeMethod, wrapped in a block to highlight/report errors. + static Future _sendMethodCall(String audioId, String method, + [dynamic arguments]) async { + try { + await channel.invokeMethod(method, arguments); + } on PlatformException catch (e) { + _logger.severe(e.message); + + // Call onError on the Audio instance. + _usingOnErrorAudios[audioId]?._onError(e.message); + // Rethrow to the calling Audio method. Callers should not rethrow if + // this instance of Audio uses onError(). + rethrow; + } + } + + /// Handle method callbacks from the native layer. + @visibleForTesting + static Future handleMethodCall(MethodCall call) async { + Map arguments = (call.arguments as Map); + final audioId = arguments[audioIdKey]; + switch (call.method) { + case onCompleteCallback: + _onCompleteNative(audioId); + break; + case onDurationCallback: + final durationSeconds = arguments[durationSecondsKey]; + _onDurationNative(audioId, durationSeconds); + break; + case onPositionCallback: + final positionSeconds = arguments[positionSecondsKey]; + _onPositionNative(audioId, positionSeconds); + break; + default: + _logger.warning('Unknown method ${call.method}'); + } + } +} diff --git a/packages/audiofileplayer/pubspec.yaml b/packages/audiofileplayer/pubspec.yaml new file mode 100644 index 0000000..42b5ddc --- /dev/null +++ b/packages/audiofileplayer/pubspec.yaml @@ -0,0 +1,64 @@ +name: audiofileplayer +description: A new flutter plugin project. +version: 0.0.1 +author: +homepage: + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + logging: + meta: + uuid: + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://www.dartlang.org/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The androidPackage and pluginClass identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + androidPackage: com.google.flutter.plugins.audiofileplayer + pluginClass: AudiofileplayerPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.io/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.io/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.io/custom-fonts/#from-packages diff --git a/packages/audiofileplayer/test/audiofileplayer_test.dart b/packages/audiofileplayer/test/audiofileplayer_test.dart new file mode 100644 index 0000000..90900a6 --- /dev/null +++ b/packages/audiofileplayer/test/audiofileplayer_test.dart @@ -0,0 +1,251 @@ +import 'dart:ui' show AppLifecycleState; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart' show WidgetsFlutterBinding; +import 'package:flutter_test/flutter_test.dart'; +import 'package:audiofileplayer/audiofileplayer.dart'; + +const _defaultPositionSeconds = 5.0; +const _defaultDurationSeconds = 10.0; +const _exceptionMessage = "Test exception"; + +void main() { + group('$Audio', () { + final methodCalls = []; + bool _throwExceptionOnNextMethodCall = false; + + setUp(() async { + // Ensures that Audio objects are able to call WidgetBinder.instance. + WidgetsFlutterBinding.ensureInitialized(); + Audio.shouldPlayWhileAppPaused = false; + Audio.channel.setMockMethodCallHandler((MethodCall methodCall) async { + methodCalls.add(methodCall); + if (_throwExceptionOnNextMethodCall) { + _throwExceptionOnNextMethodCall = false; + throw PlatformException( + code: errorCode, + message: _exceptionMessage, + ); + } + }); + methodCalls.clear(); + }); + + _mockOnCompleteCall(String audioId) => Audio.handleMethodCall( + MethodCall(onCompleteCallback, {audioIdKey: audioId})); + + _mockOnDurationCall(String audioId) => Audio.handleMethodCall(MethodCall( + onDurationCallback, + {audioIdKey: audioId, durationSecondsKey: _defaultDurationSeconds})); + + _mockOnPositionCall(String audioId) => Audio.handleMethodCall(MethodCall( + onPositionCallback, + {audioIdKey: audioId, positionSecondsKey: _defaultPositionSeconds})); + + test('one-shot playback, with dispose() before playback completion', () { + Audio.load('foo.wav') + ..play() + ..dispose(); + // Check that the Audio instance is no longer retained by the Audio class. + expect(Audio.undisposedAudiosCount, 0); + // Check sequence of native calls. + expect(methodCalls.length, 2); + expect(methodCalls[0].method, loadMethod); + expect(methodCalls[1].method, playMethod); + // Mock an on-complete call from native layer. + final audioId = (methodCalls[0].arguments as Map)[audioIdKey]; + _mockOnCompleteCall(audioId); + // Check that playback completion after dispose() triggers native resource release. + expect(methodCalls.length, 3); + expect(methodCalls[2].method, releaseMethod); + }); + + test('one-shot playback, with dispose() after playback completion', () { + final audio = Audio.load('foo.wav')..play(); + // Check that the Audio instance is being retained by the Audio class. + expect(Audio.undisposedAudiosCount, 1); + // Check sequence of native calls. + expect(methodCalls.length, 2); + expect(methodCalls[0].method, loadMethod); + expect(methodCalls[1].method, playMethod); + // Mock an onComplete call from native layer. + final audioId = (methodCalls[0].arguments as Map)[audioIdKey]; + _mockOnCompleteCall(audioId); + // Call dispose; audio is finished playing, so should native layer should + // be released immediately. + audio.dispose(); + expect(Audio.undisposedAudiosCount, 0); + expect(methodCalls.length, 3); + expect(methodCalls[2].method, releaseMethod); + }); + + test('playback, pause, dispose', () { + Audio.load('foo.wav') + ..play() + ..pause() + ..dispose(); + // Check that the Audio instance is no longer retained by the Audio class. + expect(Audio.undisposedAudiosCount, 0); + // Check sequence of native calls. + expect(methodCalls.length, 4); + expect(methodCalls[0].method, loadMethod); + expect(methodCalls[1].method, playMethod); + expect(methodCalls[2].method, pauseMethod); + expect(methodCalls[3].method, releaseMethod); + }); + + test('seek, and dispose without playback', () { + Audio.load('foo.wav') + ..seek(_defaultPositionSeconds) + ..dispose(); + // Check that the Audio instance is no longer retained by the Audio class. + expect(Audio.undisposedAudiosCount, 0); + // Check sequence of native calls. + expect(methodCalls.length, 3); + expect(methodCalls[0].method, loadMethod); + expect(methodCalls[1].method, seekMethod); + expect((methodCalls[1].arguments as Map)[positionSecondsKey], + _defaultPositionSeconds); + expect(methodCalls[2].method, releaseMethod); + }); + + test('onComplete, onPosition, onDuration called, even after dispose()', () { + bool onCompleteCalled = false; + double duration; + double position; + Audio.load('foo.wav', + onComplete: () => onCompleteCalled = true, + onDuration: (d) => duration = d, + onPosition: (p) => position = p) + ..play() + ..dispose(); + // Check that the Audio instance is retained by the Audio class while + // awaiting duration/position. + expect(Audio.undisposedAudiosCount, 0); + expect(Audio.awaitingOnCompleteAudiosCount, 1); + expect(Audio.awaitingOnDurationAudiosCount, 1); + expect(Audio.usingOnPositionAudiosCount, 1); + + // Check sequence of native calls. + expect(methodCalls.length, 2); + expect(methodCalls[0].method, loadMethod); + expect(methodCalls[1].method, playMethod); + + // Mock duration, position, and completion calls from native layer, + // check that callbacks are called. + final audioId = (methodCalls[0].arguments as Map)[audioIdKey]; + _mockOnDurationCall(audioId); + expect(duration, _defaultDurationSeconds); + + _mockOnPositionCall(audioId); + expect(position, _defaultPositionSeconds); + + _mockOnCompleteCall(audioId); + expect(onCompleteCalled, true); + + // Check that the Audio instance is no longer being retained by the Audio + // class. + expect(Audio.undisposedAudiosCount, 0); + expect(Audio.awaitingOnCompleteAudiosCount, 0); + expect(Audio.awaitingOnDurationAudiosCount, 0); + expect(Audio.usingOnPositionAudiosCount, 0); + + // After dispose() and audio completion, check native layer released. + expect(methodCalls.length, 3); + expect(methodCalls[2].method, releaseMethod); + }); + + test('remove callbacks', () { + bool onCompleteCalled = false; + double duration; + double position; + Audio.load('foo.wav', + onComplete: () => onCompleteCalled = true, + onDuration: (d) => duration = d, + onPosition: (p) => position = p) + ..play() + ..removeCallbacks() + ..dispose(); + + // Check that the Audio instance is no longer being retained by the Audio + // class. + expect(Audio.undisposedAudiosCount, 0); + expect(Audio.awaitingOnCompleteAudiosCount, 0); + expect(Audio.awaitingOnDurationAudiosCount, 0); + expect(Audio.usingOnPositionAudiosCount, 0); + + // Check sequence of native calls. + expect(methodCalls.length, 2); + expect(methodCalls[0].method, loadMethod); + expect(methodCalls[1].method, playMethod); + + // Mock duration, position, and completion calls from native layer, + // check that callbacks are _not_ called. + final audioId = (methodCalls[0].arguments as Map)[audioIdKey]; + _mockOnDurationCall(audioId); + expect(duration, null); + + _mockOnPositionCall(audioId); + expect(position, null); + + _mockOnCompleteCall(audioId); + expect(onCompleteCalled, false); + + // After dispose() and audio completion, check native layer released. + expect(methodCalls.length, 3); + expect(methodCalls[2].method, releaseMethod); + }); + + test('should pause audio when app is paused and resume when app is resumed', + () { + final audio = Audio.load('foo.wav') + ..play() + ..dispose(); + // Mock app change from active to paused (i.e. user backgrounds app). + audio.didChangeAppLifecycleState(AppLifecycleState.paused); + // Check sequence of native calls, the last of which is a call to pause. + expect(methodCalls.length, 3); + expect(methodCalls[0].method, loadMethod); + expect(methodCalls[1].method, playMethod); + expect(methodCalls[2].method, pauseMethod); + // Mock app change from paused to active (i.e. user foregrounds app). + audio.didChangeAppLifecycleState(AppLifecycleState.resumed); + // Check sequence of native calls, the last of which is a call to resume + // playback. + expect(methodCalls.length, 4); + expect(methodCalls[3].method, playMethod); + expect((methodCalls[3].arguments as Map)[playFromStartKey], false); + }); + + test('should continue to play audio while app is paused', () { + Audio.shouldPlayWhileAppPaused = true; + final audio = Audio.load('foo.wav') + ..play() + ..dispose(); + // Mock app change from active to paused (i.e. user backgrounds app). + audio.didChangeAppLifecycleState(AppLifecycleState.paused); + // Check sequence of native calls. There should not be an additional call + // to pause playback. + expect(methodCalls.length, 2); + expect(methodCalls[0].method, loadMethod); + expect(methodCalls[1].method, playMethod); + // Mock app change from paused to active (i.e. user foregrounds app). + audio.didChangeAppLifecycleState(AppLifecycleState.resumed); + // Check sequence of native calls. There should not be any additional + // calls. + expect(methodCalls.length, 2); + }); + + test('PlatformException is caught and calls onError()', () { + _throwExceptionOnNextMethodCall = true; + var errorHandler = expectAsync1((m) => expect(m, _exceptionMessage)); + Audio.load('foo.wav', onError: errorHandler); + }); + + test('PlatformException is thrown when no onError() set', () { + final audio = Audio.load('foo.wav'); + _throwExceptionOnNextMethodCall = true; + expect(audio.play(), throwsException); + }); + }); +}