├── ios ├── Assets │ └── .gitkeep ├── Classes │ ├── StereoPlugin.h │ ├── STMediaPickerController.h │ ├── STMediaPickerController.m │ └── StereoPlugin.m ├── .gitignore └── stereo.podspec ├── android ├── gradle.properties ├── settings.gradle ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── twofind │ │ └── stereo │ │ ├── AudioTrack.java │ │ └── StereoPlugin.java └── build.gradle ├── example ├── android │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── app │ │ ├── src │ │ │ └── main │ │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── twofind │ │ │ │ │ └── stereoexample │ │ │ │ │ └── MainActivity.java │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── .gitignore │ ├── settings.gradle │ ├── build.gradle │ ├── gradlew.bat │ └── gradlew ├── doc │ └── example.jpg ├── assets │ ├── songs │ │ ├── pi.mp3 │ │ └── dubstep.mp3 │ └── images │ │ └── artwork_default.png ├── ios │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── flutter_assets │ │ │ ├── kernel_blob.bin │ │ │ ├── vm_snapshot_data │ │ │ ├── assets │ │ │ │ ├── songs │ │ │ │ │ ├── pi.mp3 │ │ │ │ │ └── dubstep.mp3 │ │ │ │ └── images │ │ │ │ │ └── artwork_default.png │ │ │ ├── platform_strong.dill │ │ │ ├── isolate_snapshot_data │ │ │ ├── fonts │ │ │ │ └── MaterialIcons-Regular.ttf │ │ │ ├── AssetManifest.json │ │ │ └── FontManifest.json │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── AppDelegate.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── main.m │ │ ├── AppDelegate.m │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── WorkspaceSettings.xcsettings │ ├── .gitignore │ ├── Podfile.lock │ └── Podfile ├── .gitignore ├── .metadata ├── test │ └── widget_test.dart ├── pubspec.yaml ├── README.md ├── LICENSE └── lib │ ├── media_info_widget.dart │ ├── media_player_widget.dart │ └── main.dart ├── .gitignore ├── CHANGELOG.md ├── lib ├── stereo.dart └── src │ ├── audio_track.dart │ └── stereo.dart ├── pubspec.yaml ├── LICENSE └── README.md /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'stereo' 2 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /example/doc/example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/doc/example.jpg -------------------------------------------------------------------------------- /example/assets/songs/pi.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/assets/songs/pi.mp3 -------------------------------------------------------------------------------- /example/assets/songs/dubstep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/assets/songs/dubstep.mp3 -------------------------------------------------------------------------------- /example/assets/images/artwork_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/assets/images/artwork_default.png -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/kernel_blob.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Flutter/flutter_assets/kernel_blob.bin -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/vm_snapshot_data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Flutter/flutter_assets/vm_snapshot_data -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/assets/songs/pi.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Flutter/flutter_assets/assets/songs/pi.mp3 -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/platform_strong.dill: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Flutter/flutter_assets/platform_strong.dill -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/isolate_snapshot_data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Flutter/flutter_assets/isolate_snapshot_data -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/assets/songs/dubstep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Flutter/flutter_assets/assets/songs/dubstep.mp3 -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .idea 4 | .packages 5 | .pub/ 6 | build/ 7 | ios/.generated/ 8 | packages 9 | pubspec.lock 10 | .flutter-plugins 11 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/fonts/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Flutter/flutter_assets/fonts/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .idea 4 | .packages 5 | .pub/ 6 | build/ 7 | ios/.generated/ 8 | packages 9 | pubspec.lock 10 | 11 | # Exclude IDEA modules 12 | *.iml 13 | -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/assets/images/artwork_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Flutter/flutter_assets/assets/images/artwork_default.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | GeneratedPluginRegistrant.java 10 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2find/stereo/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.2 2 | 3 | * Updates Gradle to 4.6 version. 4 | 5 | ## 1.0.1 6 | 7 | * Fixes a bug which would interfere with the audio_recorder library. Credits to joelfaul. 8 | 9 | ## 1.0.0 10 | 11 | * Initial release 12 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/stereo.dart: -------------------------------------------------------------------------------- 1 | library stereo; 2 | 3 | import 'dart:async'; 4 | import 'dart:typed_data'; 5 | 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:flutter/services.dart'; 8 | 9 | part 'src/audio_track.dart'; 10 | part 'src/stereo.dart'; 11 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ios/Classes/StereoPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface StereoPlugin : NSObject 6 | 7 | - (StereoPlugin * _Nonnull)initWithChannel:(FlutterMethodChannel * _Nonnull)channel; 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 7 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f802cf6d16b77d5453f459e90d3f03207833b81c 8 | channel: dev 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | 13 | *.pbxuser 14 | *.mode1v3 15 | *.mode2v3 16 | *.perspectivev3 17 | 18 | !default.pbxuser 19 | !default.mode1v3 20 | !default.mode2v3 21 | !default.perspectivev3 22 | 23 | xcuserdata 24 | 25 | *.moved-aside 26 | 27 | *.pyc 28 | *sync/ 29 | Icon? 30 | .tags* 31 | 32 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter 3 | // provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to 4 | // find child widgets in the widget tree, read text, and verify that the values of widget properties 5 | // are correct. 6 | 7 | void main() { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /ios/Classes/STMediaPickerController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface STAudioTrack : NSObject 5 | 6 | + (NSDictionary * _Nonnull)toJson:(NSURL * _Nonnull)url; 7 | 8 | @end 9 | 10 | @interface STMediaPickerController : MPMediaPickerController 11 | 12 | - (STMediaPickerController * _Nonnull)initWithResult:(FlutterResult _Nonnull)result; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 7 | [GeneratedPluginRegistrant registerWithRegistry:self]; 8 | // Override point for customization after application launch. 9 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 10 | } 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/twofind/stereoexample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.twofind.stereoexample; 2 | 3 | import android.os.Bundle; 4 | 5 | import io.flutter.app.FlutterActivity; 6 | import io.flutter.plugins.GeneratedPluginRegistrant; 7 | 8 | public class MainActivity extends FlutterActivity { 9 | @Override 10 | protected void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | GeneratedPluginRegistrant.registerWith(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withInputStream { stream -> plugins.load(stream) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/AssetManifest.json: -------------------------------------------------------------------------------- 1 | {"assets/images/artwork_default.png":["assets/images/artwork_default.png"],"assets/songs/dubstep.mp3":["assets/songs/dubstep.mp3"],"packages/font_awesome_flutter/lib/fonts/fa-brands-400.ttf":["packages/font_awesome_flutter/lib/fonts/fa-brands-400.ttf"],"packages/font_awesome_flutter/lib/fonts/fa-regular-400.ttf":["packages/font_awesome_flutter/lib/fonts/fa-regular-400.ttf"],"packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf":["packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf"],"assets/songs/pi.mp3":["assets/songs/pi.mp3"]} -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/FontManifest.json: -------------------------------------------------------------------------------- 1 | [{"fonts":[{"asset":"fonts/MaterialIcons-Regular.ttf"}],"family":"MaterialIcons"},{"family":"packages/font_awesome_flutter/FontAwesomeBrands","fonts":[{"weight":400,"asset":"packages/font_awesome_flutter/lib/fonts/fa-brands-400.ttf"}]},{"family":"packages/font_awesome_flutter/FontAwesomeRegular","fonts":[{"weight":400,"asset":"packages/font_awesome_flutter/lib/fonts/fa-regular-400.ttf"}]},{"family":"packages/font_awesome_flutter/FontAwesomeSolid","fonts":[{"weight":900,"asset":"packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf"}]}] -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.0' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | *.pbxuser 16 | *.mode1v3 17 | *.mode2v3 18 | *.perspectivev3 19 | 20 | !default.pbxuser 21 | !default.mode1v3 22 | !default.mode2v3 23 | !default.perspectivev3 24 | 25 | xcuserdata 26 | 27 | *.moved-aside 28 | 29 | *.pyc 30 | *sync/ 31 | Icon? 32 | .tags* 33 | 34 | /Flutter/app.flx 35 | /Flutter/app.zip 36 | /Flutter/App.framework 37 | /Flutter/Flutter.framework 38 | /Flutter/Generated.xcconfig 39 | /ServiceDefinitions.json 40 | 41 | Pods/ 42 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: stereo 2 | description: A Flutter plugin for playing music on iOS and Android. Supports 3 | basic media controls (play, pause, stop, seek) and picking files from 4 | internal storage. 5 | version: 1.0.2 6 | author: 2find Team <2findteam@googlegroups.com> 7 | homepage: https://github.com/2find/stereo 8 | 9 | analyzer: 10 | strong-mode: true 11 | 12 | flutter: 13 | plugin: 14 | androidPackage: com.twofind.stereo 15 | pluginClass: StereoPlugin 16 | iosPrefix: ST 17 | 18 | dependencies: 19 | flutter: 20 | sdk: flutter 21 | 22 | dev_dependencies: 23 | flutter_test: 24 | sdk: flutter 25 | 26 | environment: 27 | sdk: ">=2.0.0-dev.28.0 <3.0.0" 28 | flutter: ">=0.1.4 <2.0.0" 29 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: stereo_example 2 | description: Demonstrates how to use the stereo plugin. 3 | version: 1.0.0 4 | author: 2find Team <2findteam@googlegroups.com> 5 | homepage: https://github.com/2find/stereo/example 6 | 7 | dependencies: 8 | font_awesome_flutter: "^8.0.1" 9 | intl: "^0.15.2" 10 | path_provider: "^0.4.1" 11 | flutter: 12 | sdk: flutter 13 | stereo: "^1.0.0" 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | flutter: 20 | assets: 21 | - assets/images/artwork_default.png 22 | - assets/songs/dubstep.mp3 23 | - assets/songs/pi.mp3 24 | uses-material-design: true 25 | 26 | environment: 27 | sdk: ">=2.0.0-dev.28.0 <3.0.0" 28 | flutter: ">=0.1.4 <2.0.0" 29 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Stereo Example 2 | 3 |
4 | 5 |
6 | 7 | This example shows how the library works. 8 | 9 | * The two buttons *Play dubstep.mp3* and *Play pi.mp3* load a file from the application directory and play them. These files don't have ID3 tags so no information will be displayed. 10 | 11 | * The button *Invalid URL* triggers a dialog to inform that the file is not playable. 12 | 13 | * The button *Pick file* pops an UI to pick a track from phone's internal storage. 14 | 15 | * The buttons at the bottom are controls to play, pause and stop the playback. The fourth button is just there to show the current playback state (playing or not). It's greyed out since no action is bound. 16 | -------------------------------------------------------------------------------- /ios/stereo.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'stereo' 3 | s.version = '0.1.0' 4 | s.summary = 'A Flutter plugin for playing music on iOS and Android.' 5 | s.description = <<-DESC 6 | A Flutter plugin for playing music on iOS and Android. 7 | DESC 8 | s.homepage = 'https://github.com/2find/stereo' 9 | s.license = { :file => '../LICENSE' } 10 | s.author = { '2find Team' => 'faku99dev@gmail.com' } 11 | s.source = { :path => '.' } 12 | s.source_files = 'Classes/**/*' 13 | s.public_header_files = 'Classes/**/*.h' 14 | s.dependency 'Flutter' 15 | s.frameworks = 'AVFoundation', 'MediaPlayer' 16 | s.ios.frameworks = 'UIKit' 17 | 18 | s.ios.deployment_target = '8.0' 19 | end 20 | 21 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.twofind.stereo' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.2.0' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | compileSdkVersion 27 26 | buildToolsVersion '27.0.3' 27 | 28 | defaultConfig { 29 | minSdkVersion 16 30 | targetSdkVersion 27 31 | versionCode 1 32 | versionName "1.0" 33 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 34 | } 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /android/src/main/java/com/twofind/stereo/AudioTrack.java: -------------------------------------------------------------------------------- 1 | package com.twofind.stereo; 2 | 3 | import android.media.MediaMetadataRetriever; 4 | 5 | import java.util.HashMap; 6 | 7 | public class AudioTrack { 8 | private AudioTrack() { } 9 | 10 | public static HashMap toJson(String path) { 11 | HashMap data = new HashMap<>(); 12 | 13 | MediaMetadataRetriever mmr = new MediaMetadataRetriever(); 14 | mmr.setDataSource(path); 15 | 16 | data.put("album", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)); 17 | data.put("artist", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)); 18 | data.put("title", mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)); 19 | data.put("path", path); 20 | data.put("artwork", mmr.getEmbeddedPicture()); 21 | 22 | mmr.release(); 23 | 24 | return data; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | MinimumOSVersion 28 | 8.0 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - path_provider (0.0.1): 4 | - Flutter 5 | - stereo (0.1.0): 6 | - Flutter 7 | 8 | DEPENDENCIES: 9 | - Flutter (from `/Users/faku99/opt/flutter/bin/cache/artifacts/engine/ios`) 10 | - path_provider (from `/Users/faku99/.pub-cache/hosted/pub.dartlang.org/path_provider-0.4.1/ios`) 11 | - stereo (from `/Users/faku99/.pub-cache/hosted/pub.dartlang.org/stereo-1.0.0/ios`) 12 | 13 | EXTERNAL SOURCES: 14 | Flutter: 15 | :path: "/Users/faku99/opt/flutter/bin/cache/artifacts/engine/ios" 16 | path_provider: 17 | :path: "/Users/faku99/.pub-cache/hosted/pub.dartlang.org/path_provider-0.4.1/ios" 18 | stereo: 19 | :path: "/Users/faku99/.pub-cache/hosted/pub.dartlang.org/stereo-1.0.0/ios" 20 | 21 | SPEC CHECKSUMS: 22 | Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296 23 | path_provider: 09407919825bfe3c2deae39453b7a5b44f467873 24 | stereo: 583b5fc19f590e9f526456f703766a56307d0b7f 25 | 26 | PODFILE CHECKSUM: 351e02e34b831289961ec3558a535cbd2c4965d2 27 | 28 | COCOAPODS: 1.5.3 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 2find Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 2find Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | if ENV['FLUTTER_FRAMEWORK_DIR'] == nil 5 | abort('Please set FLUTTER_FRAMEWORK_DIR to the directory containing Flutter.framework') 6 | end 7 | 8 | target 'Runner' do 9 | # Pods for Runner 10 | 11 | # Flutter Pods 12 | pod 'Flutter', :path => ENV['FLUTTER_FRAMEWORK_DIR'] 13 | 14 | if File.exists? '../.flutter-plugins' 15 | flutter_root = File.expand_path('..') 16 | File.foreach('../.flutter-plugins') { |line| 17 | plugin = line.split(pattern='=') 18 | if plugin.length == 2 19 | name = plugin[0].strip() 20 | path = plugin[1].strip() 21 | resolved_path = File.expand_path("#{path}/ios", flutter_root) 22 | pod name, :path => resolved_path 23 | else 24 | puts "Invalid plugin specification: #{line}" 25 | end 26 | } 27 | end 28 | end 29 | 30 | post_install do |installer| 31 | installer.pods_project.targets.each do |target| 32 | target.build_configurations.each do |config| 33 | config.build_settings['ENABLE_BITCODE'] = 'NO' 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stereo plugin for Flutter 2 | 3 | A Flutter plugin for playing music on iOS and Android. 4 | 5 | ## Features 6 | 7 | * Play/pause 8 | * Stop 9 | * Duration / seek to position 10 | * Load track from path 11 | * Load track from library 12 | 13 | ## Installation 14 | 15 | First, add `stereo` as a dependency in your `pubspec.yaml` file. 16 | 17 | ### Android 18 | 19 | Add the following permission to your `AndroidManifest.xml` file: 20 | * `` 21 | 22 | ### iOS 23 | 24 | Add the following key to your `Info.plist` file: 25 | * `NSAppleMusicUsageDescription` 26 | 27 | ## Example 28 | 29 | See the [example documentation](example/README.md) for more information. 30 | 31 | ## Changelog 32 | 33 | See [CHANGELOG.md](CHANGELOG.md). 34 | 35 | ## Contributing 36 | 37 | Feel free to contribute by opening issues and/or pull requests. Your feedback is very welcome! 38 | 39 | ## License 40 | 41 | This project is licensed under the MIT License. See [LICENSE](LICENSE) file for more information. 42 | 43 | ## Credits 44 | 45 | Stereo has been developed and is maintained by **faku99**, **ludelafo** and **Daxidz**. 46 | 47 | In no particular order, we credit the following people for their invaluable contributions: 48 | 49 | * **joelfaul** for fixing an interference with the [audio_recorder](https://pub.dartlang.org/packages/audio_recorder) library 50 | -------------------------------------------------------------------------------- /lib/src/audio_track.dart: -------------------------------------------------------------------------------- 1 | part of stereo; 2 | 3 | /// Represents a track. 4 | class AudioTrack { 5 | /// Track metadata. 6 | Map data; 7 | 8 | /// Default values to replace fields that are `null` at initialization. 9 | static Map defaults = new Map.from({ 10 | 'album': 'Unknown album', 11 | 'artist': 'Unknown artist', 12 | 'artwork': null, 13 | 'path': null, 14 | 'title': 'Unknown title' 15 | }); 16 | 17 | /// Track album. 18 | String get album => data['album']; 19 | 20 | /// Track artist. 21 | String get artist => data['artist']; 22 | 23 | /// Track artwork. 24 | Uint8List get artwork => data['artwork']; 25 | 26 | /// Track path. 27 | String get path => data['path']; 28 | 29 | /// Track title. 30 | String get title => data['title']; 31 | 32 | /// Creates a track by defining its fields. 33 | /// 34 | /// If a field is `null`, it will assign its default value as specified in 35 | /// the [defaults] static variable. 36 | AudioTrack( 37 | {String album, 38 | String artist, 39 | Uint8List artwork, 40 | String path, 41 | String title}) { 42 | data = { 43 | 'album': album ?? defaults['album'], 44 | 'artwork': artwork ?? defaults['artwork'], 45 | 'artist': artist ?? defaults['artist'], 46 | 'path': path ?? defaults['path'], 47 | 'title': title ?? defaults['title'] 48 | }; 49 | } 50 | 51 | /// Creates a track from existing metadata. 52 | AudioTrack.fromJson(Map data) 53 | : this( 54 | album: data['album'], 55 | artist: data['artist'], 56 | artwork: data['artwork'], 57 | path: data['path'], 58 | title: data['title']); 59 | } 60 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withInputStream { stream -> 5 | localProperties.load(stream) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | android { 18 | compileSdkVersion 27 19 | buildToolsVersion '27.0.3' 20 | 21 | lintOptions { 22 | disable 'InvalidPackage' 23 | } 24 | 25 | defaultConfig { 26 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 27 | applicationId "com.twofind.stereoexample" 28 | minSdkVersion 16 29 | targetSdkVersion 27 30 | versionCode 1 31 | versionName "1.0" 32 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 33 | } 34 | 35 | buildTypes { 36 | release { 37 | // TODO: Add your own signing config for the release build. 38 | // Signing with the debug keys for now, so `flutter run --release` works. 39 | signingConfig signingConfigs.debug 40 | } 41 | } 42 | } 43 | 44 | flutter { 45 | source '../..' 46 | } 47 | 48 | dependencies { 49 | testImplementation 'junit:junit:4.12' 50 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 51 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 52 | } 53 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Stereo Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | stereo_example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppleMusicUsageDescription 28 | 29 | UIBackgroundModes 30 | 31 | audio 32 | remote-notification 33 | 34 | UILaunchStoryboardName 35 | LaunchScreen 36 | UIMainStoryboardFile 37 | Main 38 | UIRequiredDeviceCapabilities 39 | 40 | arm64 41 | 42 | UISupportedInterfaceOrientations 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UISupportedInterfaceOrientations~ipad 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationPortraitUpsideDown 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationLandscapeRight 54 | 55 | UIViewControllerBasedStatusBarAppearance 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /example/lib/media_info_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:stereo/stereo.dart'; 3 | 4 | class MediaInfoWidget extends StatefulWidget { 5 | @override 6 | _MediaInfoState createState() => new _MediaInfoState(); 7 | } 8 | 9 | class _MediaInfoState extends State { 10 | Stereo _stereo = new Stereo(); 11 | 12 | @override 13 | void initState() { 14 | super.initState(); 15 | 16 | _stereo.currentTrackNotifier.addListener(() => setState(() {})); 17 | } 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | List widgets = [ 22 | _getArtwork(), 23 | _getTitleText(), 24 | _getSubtitleText() 25 | ]; 26 | 27 | return new Expanded( 28 | child: new Column( 29 | children: widgets, mainAxisAlignment: MainAxisAlignment.center)); 30 | } 31 | 32 | Widget _getArtwork() { 33 | Image image; 34 | 35 | if (_stereo.currentTrack?.artwork != null) { 36 | image = 37 | new Image.memory(_stereo.currentTrack.artwork, fit: BoxFit.fitHeight); 38 | } else { 39 | image = new Image.asset('assets/images/artwork_default.png', 40 | fit: BoxFit.fitHeight); 41 | } 42 | 43 | return new Expanded(child: image); 44 | } 45 | 46 | Widget _getSubtitleText() { 47 | return new Padding( 48 | padding: const EdgeInsets.symmetric(horizontal: 40.0), 49 | child: new Text( 50 | '${_stereo.currentTrack?.artist ?? 51 | AudioTrack.defaults['artist']} - ${_stereo.currentTrack 52 | ?.album ?? AudioTrack.defaults['album']}', 53 | textAlign: TextAlign.center, 54 | style: new TextStyle(fontSize: 18.0))); 55 | } 56 | 57 | Widget _getTitleText() { 58 | return new Padding( 59 | padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), 60 | child: new Text( 61 | '${_stereo.currentTrack?.title ?? AudioTrack.defaults['title']}', 62 | textAlign: TextAlign.center, 63 | overflow: TextOverflow.clip, 64 | maxLines: 1, 65 | style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 24.0))); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ios/Classes/STMediaPickerController.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | #import "STMediaPickerController.h" 6 | 7 | @implementation STAudioTrack 8 | 9 | + (NSDictionary *)toJson:(NSURL *)url { 10 | NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; 11 | 12 | NSString *queryString = [url query]; 13 | NSArray *components = [queryString componentsSeparatedByString:@"="]; 14 | // File isn't in the Music Library. 15 | if ([components count] < 2) { 16 | return data; 17 | } 18 | 19 | id trackID = [components objectAtIndex:1]; 20 | 21 | MPMediaQuery *query = [[MPMediaQuery alloc] init]; 22 | [query addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:trackID forProperty:MPMediaItemPropertyPersistentID]]; 23 | 24 | NSArray *items = [query items]; 25 | MPMediaItem *item = [items objectAtIndex:0]; 26 | 27 | UIImage *artworkImage = [item.artwork imageWithSize:CGSizeMake(100, 100)]; 28 | FlutterStandardTypedData *artworkData = [FlutterStandardTypedData typedDataWithBytes:UIImagePNGRepresentation(artworkImage)]; 29 | 30 | [data setObject:item.albumTitle forKey:@"album"]; 31 | [data setObject:item.artist forKey:@"artist"]; 32 | [data setObject:artworkData forKey:@"artwork"]; 33 | [data setObject:[url absoluteString] forKey:@"path"]; 34 | [data setObject:item.title forKey:@"title"]; 35 | 36 | return data; 37 | } 38 | 39 | @end 40 | 41 | @implementation STMediaPickerController { 42 | FlutterResult _result; 43 | } 44 | 45 | - (STMediaPickerController *)initWithResult:(FlutterResult _Nonnull)result { 46 | self = [super initWithMediaTypes:MPMediaTypeAnyAudio]; 47 | 48 | if (self) { 49 | _result = result; 50 | 51 | [self setDelegate:self]; 52 | [self setAllowsPickingMultipleItems:NO]; 53 | } 54 | 55 | return self; 56 | } 57 | 58 | #pragma mark - MPMediaPickerControllerDelegate methods 59 | 60 | - (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection * _Nonnull)mediaItemCollection { 61 | [mediaPicker dismissViewControllerAnimated:YES completion:nil]; 62 | 63 | MPMediaItem *item = mediaItemCollection.items[0]; 64 | 65 | NSDictionary *data = [STAudioTrack toJson:item.assetURL]; 66 | 67 | _result(data); 68 | _result = nil; 69 | } 70 | 71 | - (void)mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker { 72 | [mediaPicker dismissViewControllerAnimated:YES completion:nil]; 73 | 74 | _result([FlutterError errorWithCode:@"NO_TRACK_SELECTED" message:@"No track has been selected." details:nil]); 75 | _result = nil; 76 | } 77 | 78 | @end 79 | -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/lib/media_player_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 4 | 5 | import 'package:intl/intl.dart'; 6 | 7 | import 'package:stereo/stereo.dart'; 8 | 9 | class MediaPlayerWidget extends StatefulWidget { 10 | @override 11 | _MediaPlayerState createState() => new _MediaPlayerState(); 12 | } 13 | 14 | class _MediaPlayerState extends State { 15 | /// Pause icon. 16 | static const Icon _pauseIcon = const Icon(FontAwesomeIcons.pause); 17 | 18 | /// Play icon. 19 | static const Icon _playIcon = const Icon(FontAwesomeIcons.play); 20 | 21 | /// Stop icon. 22 | static const Icon _stopIcon = const Icon(FontAwesomeIcons.stop); 23 | 24 | // Used to format duration. 25 | static NumberFormat _twoDigits = new NumberFormat('00', 'en_GB'); 26 | 27 | Stereo _stereo = new Stereo(); 28 | 29 | /// Returns the duration as a formatted string. 30 | String _formatDuration(Duration duration) { 31 | return '${_twoDigits.format(duration.inSeconds ~/ 60)}:${_twoDigits 32 | .format(duration.inSeconds % 60)}'; 33 | } 34 | 35 | /// Returns the slider value. 36 | double _getSliderValue() { 37 | int position = _stereo.position.inSeconds; 38 | if (position <= 0) { 39 | return 0.0; 40 | } else if (position >= _stereo.duration.inSeconds) { 41 | return _stereo.duration.inSeconds.toDouble(); 42 | } else { 43 | return position.toDouble(); 44 | } 45 | } 46 | 47 | @override 48 | void initState() { 49 | super.initState(); 50 | 51 | _stereo.durationNotifier.addListener(() => setState(() {})); 52 | _stereo.isPlayingNotifier.addListener(() => setState(() {})); 53 | _stereo.positionNotifier.addListener(() => setState(() {})); 54 | 55 | _stereo.completionHandler = () => _stereo.stop(); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return new Column(children: [ 61 | new Wrap( 62 | alignment: WrapAlignment.spaceAround, 63 | crossAxisAlignment: WrapCrossAlignment.center, 64 | spacing: 12.0, 65 | runSpacing: 8.0, 66 | children: [ 67 | new IconButton( 68 | icon: _playIcon, iconSize: 30.0, onPressed: () => _stereo.play()), 69 | new IconButton( 70 | icon: _pauseIcon, 71 | iconSize: 30.0, 72 | onPressed: () => _stereo.pause()), 73 | new IconButton( 74 | icon: _stopIcon, iconSize: 30.0, onPressed: () => _stereo.stop()), 75 | // This button is disabled since it's only there to show the current 76 | // state of the player. 77 | new IconButton( 78 | icon: _stereo.isPlaying ? _pauseIcon : _playIcon, 79 | iconSize: 50.0, 80 | onPressed: null) 81 | ], 82 | ), 83 | new Row(mainAxisSize: MainAxisSize.max, children: [ 84 | new Container( 85 | width: 50.0, 86 | child: new Text(_formatDuration(_stereo.position), 87 | textAlign: TextAlign.left)), 88 | new Expanded( 89 | child: new Slider( 90 | value: _getSliderValue(), 91 | max: _stereo.duration.inSeconds.toDouble(), 92 | onChanged: (double newValue) => 93 | _stereo.seek(new Duration(seconds: newValue.ceil())))), 94 | new Container( 95 | width: 50.0, 96 | child: new Text('-' + _formatDuration(_stereo.remaining), 97 | textAlign: TextAlign.right)) 98 | ]) 99 | ]); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart' show rootBundle; 6 | 7 | import 'package:path_provider/path_provider.dart'; 8 | import 'package:stereo/stereo.dart'; 9 | import 'package:stereo_example/media_info_widget.dart'; 10 | 11 | import 'package:stereo_example/media_player_widget.dart'; 12 | 13 | void main() { 14 | runApp(new MyApp()); 15 | } 16 | 17 | class MyApp extends StatelessWidget { 18 | @override 19 | Widget build(BuildContext context) { 20 | return new MaterialApp( 21 | title: 'Stereo Plugin Example', home: new HomeScreen()); 22 | } 23 | } 24 | 25 | class HomeScreen extends StatefulWidget { 26 | @override 27 | _HomeScreenState createState() => new _HomeScreenState(); 28 | } 29 | 30 | class _HomeScreenState extends State { 31 | Stereo _stereo = new Stereo(); 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return new Scaffold( 36 | appBar: new AppBar(title: new Text('Stereo Plugin Example')), 37 | body: new Column(children: [ 38 | new Center( 39 | heightFactor: 1.5, 40 | child: new Text('Choose an action:', 41 | style: new TextStyle( 42 | fontWeight: FontWeight.bold, fontSize: 20.0))), 43 | new Wrap( 44 | alignment: WrapAlignment.spaceAround, 45 | spacing: 12.0, 46 | runSpacing: 8.0, 47 | children: [ 48 | new RaisedButton( 49 | child: new Text('Play dubstep.mp3'), 50 | onPressed: () => _playFile('dubstep.mp3')), 51 | new RaisedButton( 52 | child: new Text('Play pi.mp3'), 53 | onPressed: () => _playFile('pi.mp3')), 54 | new RaisedButton( 55 | child: new Text('Invalid URL'), 56 | onPressed: () => _playFile("invalid_file.mp3")), 57 | new RaisedButton( 58 | child: new Text('Pick file'), onPressed: () => _pickFile()) 59 | ]), 60 | new Container(height: 5.0), 61 | new MediaInfoWidget(), 62 | new Padding( 63 | padding: 64 | new EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), 65 | child: new MediaPlayerWidget()) 66 | ])); 67 | } 68 | 69 | Future _pickFile() async { 70 | try { 71 | AudioTrack track = await _stereo.picker(); 72 | 73 | _playFile(track.path, false); 74 | } on StereoPermissionsDeniedException catch (_) { 75 | print('ERROR: Permissions denied'); 76 | } on StereoNoTrackSelectedException { 77 | print('ERROR: No track selected'); 78 | } 79 | } 80 | 81 | Future _playFile(String file, [bool fromAppDir = true]) async { 82 | String dir = ''; 83 | 84 | if (fromAppDir) { 85 | await _copyFiles(); 86 | 87 | await getApplicationDocumentsDirectory().then( 88 | (Directory directory) => dir = 'file://' + directory.path + '/'); 89 | } 90 | 91 | try { 92 | await _stereo.load('$dir$file'); 93 | } on StereoFileNotPlayableException { 94 | var alert = new AlertDialog( 95 | title: new Text('File not playable'), 96 | content: new Text('The file you specified is not playable.')); 97 | 98 | showDialog(context: context, child: alert); 99 | } 100 | } 101 | 102 | // Don't judge the code for this method, it's for the example... 103 | Future _copyFiles() async { 104 | final Directory dir = await getApplicationDocumentsDirectory(); 105 | 106 | final File dubstepSong = new File('${dir.path}/dubstep.mp3'); 107 | final File piSong = new File('${dir.path}/pi.mp3'); 108 | 109 | if (!(await dubstepSong.exists())) { 110 | final data = await rootBundle.load('assets/songs/dubstep.mp3'); 111 | final bytes = data.buffer.asUint8List(); 112 | await dubstepSong.writeAsBytes(bytes, flush: true); 113 | } 114 | if (!(await piSong.exists())) { 115 | final data = await rootBundle.load('assets/songs/pi.mp3'); 116 | final bytes = data.buffer.asUint8List(); 117 | await piSong.writeAsBytes(bytes, flush: true); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /example/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /lib/src/stereo.dart: -------------------------------------------------------------------------------- 1 | part of stereo; 2 | 3 | /// Exception thrown when a file is not playable. 4 | class StereoFileNotPlayableException implements Exception { 5 | /// A message describing the error. 6 | String message; 7 | 8 | /// Creates a new [StereoFileNotPlayableException] with an optional error 9 | /// message. 10 | StereoFileNotPlayableException([this.message]); 11 | } 12 | 13 | /// Exception thrown when a specified position is invalid. 14 | class StereoInvalidPositionException implements Exception { 15 | /// A message describing the error. 16 | String message; 17 | 18 | /// Creates a new [StereoInvalidPositionException] with an optional error 19 | /// message. 20 | StereoInvalidPositionException([this.message]); 21 | } 22 | 23 | /// Exception thrown when the user denied permissions. 24 | class StereoPermissionsDeniedException implements Exception { 25 | /// A message describing the error. 26 | String message; 27 | 28 | /// Creates a new [StereoPermissionsDeniedException] with an optional error 29 | /// message. 30 | StereoPermissionsDeniedException([this.message]); 31 | } 32 | 33 | /// Exception thrown when the user didn't select a track. 34 | class StereoNoTrackSelectedException implements Exception { 35 | /// A message description the error. 36 | String message; 37 | 38 | /// Creates a new [StereoNoTrackSelectedException] with an optional error 39 | /// message. 40 | StereoNoTrackSelectedException([this.message]); 41 | } 42 | 43 | /// Represents an audio player. 44 | /// 45 | /// This class is a factory so it has only one instance. 46 | class Stereo { 47 | /// General instance. 48 | static Stereo _instance = new Stereo._internal(); 49 | 50 | /// Channel used to communicate with the platform. 51 | static const MethodChannel _channel = 52 | const MethodChannel('com.twofind.stereo'); 53 | 54 | /// Constructor. 55 | factory Stereo() { 56 | return _instance; 57 | } 58 | 59 | /// Internal constructor. 60 | Stereo._internal() { 61 | _channel.setMethodCallHandler(_handleMethodCall); 62 | } 63 | 64 | /// Callback called when the playing song ends. 65 | VoidCallback completionHandler; 66 | 67 | /// Notifies listener every time the playing track changes. 68 | ValueNotifier _currentTrackNotifier = new ValueNotifier(null); 69 | 70 | /// Current playing track. 71 | AudioTrack get currentTrack => _currentTrackNotifier.value; 72 | 73 | /// Notifier to get notified every time the playing track changes. 74 | ValueNotifier get currentTrackNotifier => _currentTrackNotifier; 75 | 76 | /// Notifies listeners every time the playback duration changes. 77 | ValueNotifier _durationNotifier = 78 | new ValueNotifier(new Duration(seconds: 0)); 79 | 80 | /// Playback duration. 81 | Duration get duration => _durationNotifier.value; 82 | 83 | /// Notifier to get notified every time the playback duration changes. 84 | ValueNotifier get durationNotifier => _durationNotifier; 85 | 86 | /// Notifies listeners every time the Stereo player state changes. 87 | ValueNotifier _isPlayingNotifier = new ValueNotifier(false); 88 | 89 | /// Whether the Stereo player is playing. 90 | bool get isPlaying => _isPlayingNotifier.value; 91 | 92 | /// Notifier to get notified every time the Stereo player state changes. 93 | ValueNotifier get isPlayingNotifier => _isPlayingNotifier; 94 | 95 | /// Notifies listeners every time the playback position changes. 96 | ValueNotifier _positionNotifier = 97 | new ValueNotifier(new Duration(seconds: 0)); 98 | 99 | /// Playback position. 100 | Duration get position => _positionNotifier.value; 101 | 102 | /// Notifier to get notified every time the playback position changes. 103 | ValueNotifier get positionNotifier => _positionNotifier; 104 | 105 | /// Remaining time. 106 | /// 107 | /// No notifier has been made available for this member since it relies on 108 | /// the [durationNotifier] and [positionNotifier] values. 109 | Duration get remaining => _durationNotifier.value - _positionNotifier.value; 110 | 111 | /// Sets the data source (URI path) to use. 112 | /// 113 | /// Throws a [StereoFileNotPlayableException] if the specified [uri] points to 114 | /// a file which is not playable. 115 | Future load(String uri) async { 116 | int rc = await _channel.invokeMethod('app.load', uri); 117 | 118 | _isPlayingNotifier.value = await _isPlaying(); 119 | 120 | if (rc == 1) { 121 | throw new StereoFileNotPlayableException(); 122 | } 123 | } 124 | 125 | /// Pauses playback. 126 | Future pause() async { 127 | await _channel.invokeMethod('app.pause'); 128 | 129 | _isPlayingNotifier.value = await _isPlaying(); 130 | } 131 | 132 | /// Shows an UI to pick a track from storage. 133 | /// 134 | /// Returns an [AudioTrack] if the action was successful, or `null` if the 135 | /// user cancelled the action. 136 | /// 137 | /// Throws a [StereoPermissionsDeniedException] if the user denied 138 | /// permissions. 139 | /// 140 | /// Throws a [StereoNoTrackSelectedException] if the user didn't select a 141 | /// track. 142 | Future picker() async { 143 | Map data; 144 | 145 | try { 146 | data = await _channel.invokeMethod('app.picker'); 147 | } on PlatformException catch (e) { 148 | if (e.code == 'STORAGE_PERMISSION_DENIED') { 149 | throw new StereoPermissionsDeniedException(e.message); 150 | } else if (e.code == 'NO_TRACK_SELECTED') { 151 | throw new StereoNoTrackSelectedException(e.message); 152 | } else { 153 | rethrow; 154 | } 155 | } 156 | 157 | return new AudioTrack.fromJson(data); 158 | } 159 | 160 | /// Starts or resumes playback. 161 | Future play() async { 162 | await _channel.invokeMethod('app.play'); 163 | 164 | _isPlayingNotifier.value = await _isPlaying(); 165 | } 166 | 167 | /// Seeks to specified time [position]. 168 | Future seek(Duration position) async { 169 | if (position.inSeconds < 0 || position.inSeconds > duration.inSeconds) { 170 | throw new StereoInvalidPositionException(); 171 | } 172 | 173 | await _channel.invokeMethod('app.seek', position.inSeconds); 174 | } 175 | 176 | /// Stops playback. 177 | Future stop() async { 178 | await _channel.invokeMethod('app.stop'); 179 | 180 | _currentTrackNotifier.value = null; 181 | _isPlayingNotifier.value = await _isPlaying(); 182 | } 183 | 184 | /// Handles method calls from platform. 185 | Future _handleMethodCall(MethodCall call) async { 186 | switch (call.method) { 187 | case 'platform.completion': 188 | completionHandler(); 189 | break; 190 | case 'platform.currentTrack': 191 | _currentTrackNotifier.value = new AudioTrack.fromJson(call.arguments); 192 | break; 193 | case 'platform.duration': 194 | _durationNotifier.value = new Duration(seconds: call.arguments); 195 | break; 196 | case 'platform.isPlaying': 197 | _isPlayingNotifier.value = call.arguments; 198 | break; 199 | case 'platform.position': 200 | _positionNotifier.value = new Duration(seconds: call.arguments); 201 | break; 202 | default: 203 | print('[ERROR] Channel method ${call.method} not implemented.'); 204 | } 205 | } 206 | 207 | /// Returns `true` if the player is playing music, `false` otherwise. 208 | Future _isPlaying() async { 209 | return await _channel.invokeMethod('app.isPlaying'); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /android/src/main/java/com/twofind/stereo/StereoPlugin.java: -------------------------------------------------------------------------------- 1 | package com.twofind.stereo; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.ContentUris; 6 | import android.content.Intent; 7 | import android.database.Cursor; 8 | import android.media.MediaPlayer; 9 | import android.net.Uri; 10 | import android.os.Environment; 11 | import android.os.Handler; 12 | import android.provider.DocumentsContract; 13 | import android.provider.MediaStore; 14 | import io.flutter.plugin.common.MethodCall; 15 | import io.flutter.plugin.common.MethodChannel; 16 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 17 | import io.flutter.plugin.common.MethodChannel.Result; 18 | import io.flutter.plugin.common.PluginRegistry; 19 | import io.flutter.plugin.common.PluginRegistry.Registrar; 20 | 21 | import java.io.IOException; 22 | 23 | /** 24 | * StereoPlugin 25 | */ 26 | public class StereoPlugin implements MethodCallHandler, PluginRegistry.ActivityResultListener, PluginRegistry.RequestPermissionsResultListener { 27 | private MediaPlayer mediaPlayer; 28 | private static MethodChannel channel; 29 | 30 | public static final int REQUEST_PICKER_CODE = 1; 31 | public static final int REQUEST_PERMISSIONS_CODE = 2; 32 | 33 | // Flutter main activity. 34 | private Activity activity; 35 | 36 | // To handle position updates. 37 | private final Handler handler = new Handler(); 38 | 39 | // Flutter result. 40 | private Result pendingResult; 41 | 42 | public StereoPlugin(Activity activity) { 43 | this.activity = activity; 44 | } 45 | 46 | /** 47 | * Plugin registration. 48 | */ 49 | public static void registerWith(Registrar registrar) { 50 | StereoPlugin instance = new StereoPlugin(registrar.activity()); 51 | 52 | channel = new MethodChannel(registrar.messenger(), "com.twofind.stereo"); 53 | channel.setMethodCallHandler(instance); 54 | registrar.addActivityResultListener(instance); 55 | registrar.addRequestPermissionsResultListener(instance); 56 | } 57 | 58 | @Override 59 | public void onMethodCall(MethodCall call, Result result) { 60 | switch (call.method) { 61 | // isPlaying() method. 62 | case "app.isPlaying": 63 | result.success(isPlaying()); 64 | break; 65 | 66 | // load() method. 67 | case "app.load": 68 | if (call.arguments != null) { 69 | if (!(call.arguments instanceof String)) { 70 | result.error("WRONG_FORMAT", "The specified URL must be a string.", null); 71 | } 72 | 73 | String path = (String) call.arguments; 74 | 75 | result.success(load(path)); 76 | } else { 77 | result.error("NO_URL", "No URL was specified.", null); 78 | } 79 | break; 80 | 81 | // pause() method. 82 | case "app.pause": 83 | pause(); 84 | 85 | result.success(null); 86 | break; 87 | 88 | case "app.picker": 89 | if (pendingResult != null) { 90 | pendingResult.error("MULTIPLE_REQUESTS", "Cannot make multiple requests.", null); 91 | pendingResult = null; 92 | } 93 | 94 | pendingResult = result; 95 | 96 | picker(); 97 | 98 | break; 99 | 100 | // play() method. 101 | case "app.play": 102 | play(); 103 | 104 | result.success(null); 105 | break; 106 | 107 | // seek() method. 108 | case "app.seek": 109 | if (call.arguments != null) { 110 | if (!(call.arguments instanceof Integer)) { 111 | result.error("INVALID_POSITION_TYPE", "Position must be specified by an integer.", null); 112 | } 113 | 114 | int seconds = (int) call.arguments; 115 | 116 | result.success(seek(seconds)); 117 | } else { 118 | result.error("NO_POSITION", "No position was specified.", null); 119 | } 120 | break; 121 | 122 | // stop() method. 123 | case "app.stop": 124 | stop(); 125 | 126 | result.success(null); 127 | break; 128 | 129 | // Method not implemented. 130 | default: 131 | result.notImplemented(); 132 | break; 133 | } 134 | } 135 | 136 | private boolean isPlaying() { 137 | return mediaPlayer != null && mediaPlayer.isPlaying(); 138 | } 139 | 140 | private int load(String path) { 141 | stop(); 142 | 143 | mediaPlayer = new MediaPlayer(); 144 | 145 | try { 146 | mediaPlayer.setDataSource(path); 147 | mediaPlayer.prepare(); 148 | } catch (IOException e) { 149 | channel.invokeMethod("platform.duration", 0); 150 | return 1; 151 | } 152 | 153 | // Send duration to the application. 154 | channel.invokeMethod("platform.duration", mediaPlayer.getDuration() / 1000); 155 | channel.invokeMethod("platform.currentTrack", AudioTrack.toJson(path)); 156 | 157 | mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 158 | @Override 159 | public void onCompletion(MediaPlayer mediaPlayer) { 160 | channel.invokeMethod("platform.completion", null); 161 | } 162 | }); 163 | 164 | return 0; 165 | } 166 | 167 | private void pause() { 168 | if (mediaPlayer != null) { 169 | mediaPlayer.pause(); 170 | // Stop sending position to the application. 171 | handler.removeCallbacks(updatePosition); 172 | } 173 | } 174 | 175 | private void picker() { 176 | // Request permissions. 177 | activity.requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSIONS_CODE); 178 | } 179 | 180 | private void play() { 181 | if (mediaPlayer != null) { 182 | // Start sending position to the application. 183 | handler.post(updatePosition); 184 | mediaPlayer.start(); 185 | } 186 | } 187 | 188 | private int seek(int seconds) { 189 | if (mediaPlayer != null) { 190 | mediaPlayer.seekTo(seconds * 1000); 191 | // Update position. 192 | channel.invokeMethod("platform.position", mediaPlayer.getCurrentPosition() / 1000); 193 | 194 | return 0; 195 | } 196 | 197 | return 1; 198 | } 199 | 200 | private void stop() { 201 | if (mediaPlayer != null) { 202 | // Reset duration and position. 203 | handler.removeCallbacks(updatePosition); 204 | channel.invokeMethod("platform.duration", 0); 205 | channel.invokeMethod("platform.position", 0); 206 | 207 | mediaPlayer.stop(); 208 | mediaPlayer.release(); 209 | 210 | mediaPlayer = null; 211 | } 212 | } 213 | 214 | private final Runnable updatePosition = new Runnable() { 215 | @Override 216 | public void run() { 217 | try { 218 | if (!mediaPlayer.isPlaying()) { 219 | handler.removeCallbacks(updatePosition); 220 | } 221 | 222 | // Send position (seconds) to the application. 223 | channel.invokeMethod("platform.position", mediaPlayer.getCurrentPosition() / 1000); 224 | 225 | // Update every 200ms. 226 | handler.postDelayed(updatePosition, 200); 227 | } catch (Exception e) { 228 | e.printStackTrace(); 229 | } 230 | } 231 | }; 232 | 233 | @Override 234 | public boolean onActivityResult(int requestCode, int resultCode, Intent data) { 235 | switch (requestCode) { 236 | case REQUEST_PICKER_CODE: 237 | if (resultCode == Activity.RESULT_OK) { 238 | Uri uri = data.getData(); 239 | String path = getPath(uri); 240 | 241 | // Return metadata to the library. 242 | pendingResult.success(AudioTrack.toJson(path)); 243 | pendingResult = null; 244 | 245 | return true; 246 | } else { 247 | pendingResult.error("NO_TRACK_SELECTED", "No track has been selected.", null); 248 | pendingResult = null; 249 | 250 | return false; 251 | } 252 | 253 | default: 254 | return false; 255 | } 256 | } 257 | 258 | /* 259 | * Credits: https://stackoverflow.com/a/36129285/3238070 260 | */ 261 | private String getPath(Uri uri) { 262 | // DocumentProvider. 263 | if (DocumentsContract.isDocumentUri(activity, uri)) { 264 | final String documentId = DocumentsContract.getDocumentId(uri); 265 | final String[] split = documentId.split(":"); 266 | // final String type = split[0]; 267 | 268 | Uri contentUri; 269 | 270 | switch (uri.getAuthority()) { 271 | // ExternalStorageProvider 272 | case "com.android.externalstorage.documents": 273 | return Environment.getExternalStorageDirectory() + "/" + split[1]; 274 | 275 | // DownloadsProvider. 276 | case "com.android.providers.downloads.documents": 277 | // Treat 'raw' files. Don't know if that's the best way to do this, consider it as a temporary fix. 278 | if (documentId != null && documentId.startsWith("raw:")) { 279 | return documentId.substring("raw:".length()); 280 | } 281 | contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(documentId)); 282 | 283 | return getDataColumn(contentUri, null, null); 284 | 285 | // MediaProvider. 286 | case "com.android.providers.media.documents": 287 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 288 | final String selection = "_id=?"; 289 | final String[] selectionArgs = new String[]{split[1]}; 290 | 291 | return getDataColumn(contentUri, selection, selectionArgs); 292 | 293 | default: 294 | return null; 295 | } 296 | } 297 | // Media Store. 298 | else if (uri.getScheme().equals("content")) { 299 | return getDataColumn(uri, null, null); 300 | } 301 | // File. 302 | else if (uri.getScheme().equals("file")) { 303 | return uri.getPath(); 304 | } 305 | 306 | return null; 307 | } 308 | 309 | private String getDataColumn(Uri uri, String selection, String[] selectionArgs) { 310 | Cursor cursor = null; 311 | final String column = "_data"; 312 | final String[] projection = new String[]{column}; 313 | 314 | try { 315 | cursor = activity.getContentResolver().query(uri, projection, selection, selectionArgs, null); 316 | if (cursor != null && cursor.moveToFirst()) { 317 | return cursor.getString(cursor.getColumnIndexOrThrow(column)); 318 | } 319 | } finally { 320 | if (cursor != null) { 321 | cursor.close(); 322 | } 323 | } 324 | 325 | return null; 326 | } 327 | 328 | @Override 329 | public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] resultCodes) { 330 | switch(requestCode) { 331 | case REQUEST_PERMISSIONS_CODE: 332 | // Permission granted. 333 | if (resultCodes[0] == 0) { 334 | Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 335 | intent.setType("audio/*"); 336 | 337 | activity.startActivityForResult(Intent.createChooser(intent, "Open audio file"), REQUEST_PICKER_CODE); 338 | 339 | return true; 340 | } else { 341 | pendingResult.error("STORAGE_PERMISSION_DENIED", "EXTERNAL_STORAGE permission denied by user.", null); 342 | pendingResult = null; 343 | } 344 | } 345 | 346 | return false; 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /ios/Classes/StereoPlugin.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | #import "STMediaPickerController.h" 6 | #import "StereoPlugin.h" 7 | 8 | @implementation StereoPlugin { 9 | FlutterMethodChannel *_channel; 10 | FlutterViewController *_flutterController; 11 | BOOL _isPlaying; 12 | AVPlayer *_player; 13 | FlutterResult _result; 14 | id _timeObserver; 15 | } 16 | 17 | + (void)registerWithRegistrar:(NSObject *)registrar { 18 | FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"com.twofind.stereo" binaryMessenger:[registrar messenger]]; 19 | StereoPlugin* instance = [[StereoPlugin alloc] initWithChannel:channel]; 20 | 21 | [registrar addMethodCallDelegate:instance channel:channel]; 22 | [registrar addApplicationDelegate:instance]; 23 | } 24 | 25 | - (StereoPlugin *)initWithChannel:(FlutterMethodChannel * _Nonnull)channel { 26 | self = [super init]; 27 | 28 | if (self) { 29 | _channel = channel; 30 | } 31 | 32 | return self; 33 | } 34 | 35 | #pragma mark - UIApplicationDelegate methods 36 | 37 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 38 | 39 | // Don't begin audio session until we need it so that it doesn't interfere with recording 40 | return YES; 41 | } 42 | 43 | - (void)applicationWillTerminate:(UIApplication *)application { 44 | [self _endAudioSession]; 45 | } 46 | 47 | #pragma mark - FlutterPlugin methods 48 | 49 | - (void)handleMethodCall:(FlutterMethodCall * _Nonnull)call result:(FlutterResult _Nonnull)result { 50 | // isPlaying() method. 51 | if ([@"app.isPlaying" isEqualToString:call.method]) { 52 | result(@([self _isPlaying])); 53 | } 54 | // load() method. 55 | else if ([@"app.load" isEqualToString:call.method]) { 56 | if (call.arguments != nil) { 57 | if (![call.arguments isKindOfClass:[NSString class]]) { 58 | result([FlutterError errorWithCode:@"WRONG_FORMAT" message:@"The specified URL must be a string." details:nil]); 59 | } 60 | 61 | NSString *arg = (NSString *)call.arguments; 62 | if (![arg hasPrefix:@"ipod-library"]) { 63 | [NSString stringWithFormat:@"file://%@", arg]; 64 | } 65 | 66 | NSURL *url = [NSURL URLWithString: arg]; 67 | 68 | result(@([self _loadItemWithURL:url])); 69 | } 70 | else { 71 | result([FlutterError errorWithCode:@"NO_URL" message:@"No URL was specified." details:nil]); 72 | } 73 | } 74 | // pause() method. 75 | else if ([@"app.pause" isEqualToString:call.method]) { 76 | [self _pause]; 77 | 78 | result(nil); 79 | } 80 | // picker() method. 81 | else if ([@"app.picker" isEqualToString:call.method]) { 82 | if (_result != nil) { 83 | _result([FlutterError errorWithCode:@"MULTIPLE_REQUESTS" message:@"Cannot make multiple requests." details:nil]); 84 | 85 | _result = nil; 86 | } 87 | _result = result; 88 | 89 | [self _picker]; 90 | } 91 | // play() method. 92 | else if ([@"app.play" isEqualToString:call.method]) { 93 | [self _play]; 94 | 95 | result(nil); 96 | } 97 | // seek() method. 98 | else if ([@"app.seek" isEqualToString:call.method]) { 99 | if (call.arguments != nil) { 100 | if (![call.arguments isKindOfClass:[NSNumber class]]) { 101 | result([FlutterError errorWithCode:@"INVALID_POSITION_TYPE" message:@"Position must be specified by an integer." details:nil]); 102 | } 103 | 104 | int seconds = [(NSNumber *)call.arguments intValue]; 105 | 106 | [self _seek:seconds]; 107 | 108 | result(nil); 109 | } else { 110 | result([FlutterError errorWithCode:@"NO_POSITION" message:@"No position was specified." details:nil]); 111 | } 112 | } 113 | // stop() method. 114 | else if ([@"app.stop" isEqualToString:call.method]) { 115 | [self _stop]; 116 | 117 | result(nil); 118 | } 119 | // Method not implemented. 120 | else { 121 | result(FlutterMethodNotImplemented); 122 | } 123 | } 124 | 125 | #pragma mark - Private methods 126 | 127 | - (void)_beginAudioSession { 128 | NSError *error; 129 | AVAudioSession *session = [AVAudioSession sharedInstance]; 130 | 131 | [session setCategory:AVAudioSessionCategoryPlayback error:&error]; 132 | // If an error occured, display an alert. 133 | if (error != nil) { 134 | [self _showMediaPlayerAlert]; 135 | } 136 | 137 | [session setActive:YES error:&error]; 138 | if (error != nil) { 139 | [self _showMediaPlayerAlert]; 140 | } 141 | 142 | _player = [[AVPlayer alloc] initWithPlayerItem:nil]; 143 | 144 | /* TODO: Wait until Android part is implemented. 145 | [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; 146 | 147 | [[[MPRemoteCommandCenter sharedCommandCenter] pauseCommand] addTarget:self action:@selector(_notifyPlayPause:)]; 148 | [[[MPRemoteCommandCenter sharedCommandCenter] playCommand] addTarget:self action:@selector(_notifyPlayPause:)]; 149 | [[[MPRemoteCommandCenter sharedCommandCenter] togglePlayPauseCommand] addTarget:self action:@selector(_notifyPlayPause:)]; */ 150 | } 151 | 152 | -(void)_completionHandler:(NSNotification *)notification { 153 | [_channel invokeMethod:@"platform.completion" arguments:nil]; 154 | } 155 | 156 | - (void)_endAudioSession { 157 | AVAudioSession *session = [AVAudioSession sharedInstance]; 158 | [session setActive:NO error:nil]; 159 | _player = nil; 160 | [[UIApplication sharedApplication] endReceivingRemoteControlEvents]; 161 | } 162 | 163 | -(bool) _isPlaying { 164 | return _isPlaying; 165 | } 166 | 167 | - (int)_loadItemWithURL:(NSURL * _Nonnull)url { 168 | 169 | if (_player == nil) { 170 | [self _beginAudioSession]; 171 | } else { 172 | [self _pause]; 173 | } 174 | 175 | AVAsset *asset = [AVAsset assetWithURL:url]; 176 | NSArray *assetKeys = @[@"playable", @"hasProtectedContent"]; 177 | 178 | // Set position to 00:00. 179 | [_channel invokeMethod:@"platform.position" arguments:@(0)]; 180 | 181 | // If the asset is not playable, we return `1`. We do this at this point so 182 | // the player is not going in a broken state. 183 | if (asset.playable == 0) { 184 | // Set duration at 00:00. 185 | [_channel invokeMethod:@"platform.duration" arguments:@(0)]; 186 | 187 | return 1; 188 | } 189 | 190 | AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset automaticallyLoadedAssetKeys:assetKeys]; 191 | 192 | [_player replaceCurrentItemWithPlayerItem:item]; 193 | 194 | // Send new track to the application. 195 | NSDictionary *metadata = [STAudioTrack toJson:url]; 196 | [_channel invokeMethod:@"platform.currentTrack" arguments:metadata]; 197 | 198 | // Send new duration to the application. 199 | int seconds = (int)CMTimeGetSeconds(asset.duration); 200 | [_channel invokeMethod:@"platform.duration" arguments:@(seconds)]; 201 | 202 | // Create a weak reference to `self` so don't go into a retain cycle. 203 | // Credits to: https://stackoverflow.com/a/14556706/3238070 204 | __unsafe_unretained typeof(self) weakSelf = self; 205 | 206 | // Send position to the application every 200ms. 207 | CMTime interval = CMTimeMakeWithSeconds(0.2, NSEC_PER_SEC); 208 | _timeObserver = [_player addPeriodicTimeObserverForInterval:interval queue:nil usingBlock:^(CMTime time) { 209 | [weakSelf _updatePosition:time]; 210 | }]; 211 | 212 | // Add notification handler when item is done playing. 213 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_completionHandler:) name:AVPlayerItemDidPlayToEndTimeNotification object:item]; 214 | 215 | return 0; 216 | } 217 | 218 | /* TODO: Wait until Android part is implemented. How to manage this option? 219 | * Parameter or compilation option? 220 | - (MPRemoteCommandHandlerStatus)_notifyPlayPause:(MPRemoteCommandEvent *)event { 221 | [_channel invokeMethod:@"event.togglePlayPause" arguments:nil]; 222 | 223 | return MPRemoteCommandHandlerStatusSuccess; 224 | } */ 225 | 226 | - (void)_pause { 227 | _isPlaying = false; 228 | 229 | [_player pause]; 230 | [_player removeTimeObserver:_timeObserver]; 231 | _timeObserver = nil; 232 | } 233 | 234 | - (void)_picker { 235 | STMediaPickerController *picker = [[STMediaPickerController alloc] initWithResult:_result]; 236 | 237 | // If Flutter controller isn't initialized, do it. 238 | if (_flutterController == nil) { 239 | _flutterController = (FlutterViewController *)[[UIApplication sharedApplication] keyWindow].rootViewController; 240 | } 241 | 242 | // Show controller. 243 | [_flutterController presentViewController:picker animated:YES completion:nil]; 244 | } 245 | 246 | - (void)_play { 247 | if ([_player currentItem] != nil) { 248 | _isPlaying = true; 249 | 250 | // Create a weak reference to `self` so don't go into a retain cycle. 251 | // Credits to: https://stackoverflow.com/a/14556706/3238070 252 | __unsafe_unretained typeof(self) weakSelf = self; 253 | 254 | // Send position to the application every 200ms. 255 | CMTime interval = CMTimeMakeWithSeconds(0.2, NSEC_PER_SEC); 256 | _timeObserver = [_player addPeriodicTimeObserverForInterval:interval queue:nil usingBlock:^(CMTime time) { 257 | [weakSelf _updatePosition:time]; 258 | }]; 259 | [_player play]; 260 | } 261 | } 262 | 263 | - (void)_seek:(int)seconds { 264 | CMTime time = CMTimeMake(seconds, 1); 265 | [_player seekToTime:time]; 266 | 267 | // Update position even if the player isn't playing. 268 | [_channel invokeMethod:@"platform.position" arguments:@(seconds)]; 269 | } 270 | 271 | - (void)_stop { 272 | [_player pause]; 273 | [_player replaceCurrentItemWithPlayerItem:nil]; 274 | 275 | // Reset duration and position. 276 | [_channel invokeMethod:@"platform.duration" arguments:@(0)]; 277 | [_channel invokeMethod:@"platform.position" arguments:@(0)]; 278 | 279 | _isPlaying = false; 280 | 281 | [self _endAudioSession]; 282 | } 283 | 284 | - (void)_showMediaPlayerAlert { 285 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:@"There was an error with the music player. Please restart the app." preferredStyle:UIAlertControllerStyleAlert]; 286 | UIAlertAction *okButton = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]; 287 | 288 | [alert addAction:okButton]; 289 | [_flutterController presentViewController:alert animated:YES completion:nil]; 290 | } 291 | 292 | - (void)_updatePosition:(CMTime)time { 293 | if (_isPlaying) { 294 | int seconds = (int)CMTimeGetSeconds(time); 295 | 296 | [_channel invokeMethod:@"platform.position" arguments:@(seconds)]; 297 | } 298 | } 299 | 300 | @end 301 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 12 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 13 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 14 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 15 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 16 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 18 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 19 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 20 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 21 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 22 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 23 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 24 | F6C42943292EA137DA7E44AE /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 623AC0BD734F52C3AA512567 /* libPods-Runner.a */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXCopyFilesBuildPhase section */ 28 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 29 | isa = PBXCopyFilesBuildPhase; 30 | buildActionMask = 2147483647; 31 | dstPath = ""; 32 | dstSubfolderSpec = 10; 33 | files = ( 34 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 35 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 36 | ); 37 | name = "Embed Frameworks"; 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXCopyFilesBuildPhase section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 44 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 45 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 46 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 47 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 48 | 623AC0BD734F52C3AA512567 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 50 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 51 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 52 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 53 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 54 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 55 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 57 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 58 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 59 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 60 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61 | /* End PBXFileReference section */ 62 | 63 | /* Begin PBXFrameworksBuildPhase section */ 64 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 69 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 70 | F6C42943292EA137DA7E44AE /* libPods-Runner.a in Frameworks */, 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXFrameworksBuildPhase section */ 75 | 76 | /* Begin PBXGroup section */ 77 | 4B51FE7278D1526828105D58 /* Pods */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | ); 81 | name = Pods; 82 | sourceTree = ""; 83 | }; 84 | 9740EEB11CF90186004384FC /* Flutter */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 3B80C3931E831B6300D905FE /* App.framework */, 88 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 89 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 90 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 91 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 92 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 93 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 94 | ); 95 | name = Flutter; 96 | sourceTree = ""; 97 | }; 98 | 97C146E51CF9000F007C117D = { 99 | isa = PBXGroup; 100 | children = ( 101 | 9740EEB11CF90186004384FC /* Flutter */, 102 | 97C146F01CF9000F007C117D /* Runner */, 103 | 97C146EF1CF9000F007C117D /* Products */, 104 | 4B51FE7278D1526828105D58 /* Pods */, 105 | AB51649119EA3A2FEB36A29D /* Frameworks */, 106 | ); 107 | sourceTree = ""; 108 | }; 109 | 97C146EF1CF9000F007C117D /* Products */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 97C146EE1CF9000F007C117D /* Runner.app */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | 97C146F01CF9000F007C117D /* Runner */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 121 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 122 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 123 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 124 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 125 | 97C147021CF9000F007C117D /* Info.plist */, 126 | 97C146F11CF9000F007C117D /* Supporting Files */, 127 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 128 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 129 | ); 130 | path = Runner; 131 | sourceTree = ""; 132 | }; 133 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 97C146F21CF9000F007C117D /* main.m */, 137 | ); 138 | name = "Supporting Files"; 139 | sourceTree = ""; 140 | }; 141 | AB51649119EA3A2FEB36A29D /* Frameworks */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 623AC0BD734F52C3AA512567 /* libPods-Runner.a */, 145 | ); 146 | name = Frameworks; 147 | sourceTree = ""; 148 | }; 149 | /* End PBXGroup section */ 150 | 151 | /* Begin PBXNativeTarget section */ 152 | 97C146ED1CF9000F007C117D /* Runner */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 155 | buildPhases = ( 156 | EA9D86F7FBB579DC714B314A /* [CP] Check Pods Manifest.lock */, 157 | 9740EEB61CF901F6004384FC /* Run Script */, 158 | 97C146EA1CF9000F007C117D /* Sources */, 159 | 97C146EB1CF9000F007C117D /* Frameworks */, 160 | 97C146EC1CF9000F007C117D /* Resources */, 161 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 162 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 163 | 78CD0B3E8E8A1110EFEF4C38 /* [CP] Embed Pods Frameworks */, 164 | ); 165 | buildRules = ( 166 | ); 167 | dependencies = ( 168 | ); 169 | name = Runner; 170 | productName = Runner; 171 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 172 | productType = "com.apple.product-type.application"; 173 | }; 174 | /* End PBXNativeTarget section */ 175 | 176 | /* Begin PBXProject section */ 177 | 97C146E61CF9000F007C117D /* Project object */ = { 178 | isa = PBXProject; 179 | attributes = { 180 | LastUpgradeCheck = 0830; 181 | ORGANIZATIONNAME = "The Chromium Authors"; 182 | TargetAttributes = { 183 | 97C146ED1CF9000F007C117D = { 184 | CreatedOnToolsVersion = 7.3.1; 185 | DevelopmentTeam = NTA2J8MPNL; 186 | SystemCapabilities = { 187 | com.apple.BackgroundModes = { 188 | enabled = 1; 189 | }; 190 | }; 191 | }; 192 | }; 193 | }; 194 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 195 | compatibilityVersion = "Xcode 3.2"; 196 | developmentRegion = English; 197 | hasScannedForEncodings = 0; 198 | knownRegions = ( 199 | en, 200 | Base, 201 | ); 202 | mainGroup = 97C146E51CF9000F007C117D; 203 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 204 | projectDirPath = ""; 205 | projectRoot = ""; 206 | targets = ( 207 | 97C146ED1CF9000F007C117D /* Runner */, 208 | ); 209 | }; 210 | /* End PBXProject section */ 211 | 212 | /* Begin PBXResourcesBuildPhase section */ 213 | 97C146EC1CF9000F007C117D /* Resources */ = { 214 | isa = PBXResourcesBuildPhase; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 218 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 219 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 220 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 221 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 222 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 223 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 224 | ); 225 | runOnlyForDeploymentPostprocessing = 0; 226 | }; 227 | /* End PBXResourcesBuildPhase section */ 228 | 229 | /* Begin PBXShellScriptBuildPhase section */ 230 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 231 | isa = PBXShellScriptBuildPhase; 232 | buildActionMask = 2147483647; 233 | files = ( 234 | ); 235 | inputPaths = ( 236 | ); 237 | name = "Thin Binary"; 238 | outputPaths = ( 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | shellPath = /bin/sh; 242 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 243 | }; 244 | 78CD0B3E8E8A1110EFEF4C38 /* [CP] Embed Pods Frameworks */ = { 245 | isa = PBXShellScriptBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | ); 249 | inputPaths = ( 250 | "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", 251 | "${PODS_ROOT}/../../../../../../opt/flutter/bin/cache/artifacts/engine/ios/Flutter.framework", 252 | ); 253 | name = "[CP] Embed Pods Frameworks"; 254 | outputPaths = ( 255 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | shellPath = /bin/sh; 259 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 260 | showEnvVarsInLog = 0; 261 | }; 262 | 9740EEB61CF901F6004384FC /* Run Script */ = { 263 | isa = PBXShellScriptBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | ); 267 | inputPaths = ( 268 | ); 269 | name = "Run Script"; 270 | outputPaths = ( 271 | ); 272 | runOnlyForDeploymentPostprocessing = 0; 273 | shellPath = /bin/sh; 274 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 275 | }; 276 | EA9D86F7FBB579DC714B314A /* [CP] Check Pods Manifest.lock */ = { 277 | isa = PBXShellScriptBuildPhase; 278 | buildActionMask = 2147483647; 279 | files = ( 280 | ); 281 | inputPaths = ( 282 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 283 | "${PODS_ROOT}/Manifest.lock", 284 | ); 285 | name = "[CP] Check Pods Manifest.lock"; 286 | outputPaths = ( 287 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | shellPath = /bin/sh; 291 | 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"; 292 | showEnvVarsInLog = 0; 293 | }; 294 | /* End PBXShellScriptBuildPhase section */ 295 | 296 | /* Begin PBXSourcesBuildPhase section */ 297 | 97C146EA1CF9000F007C117D /* Sources */ = { 298 | isa = PBXSourcesBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 302 | 97C146F31CF9000F007C117D /* main.m in Sources */, 303 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 304 | ); 305 | runOnlyForDeploymentPostprocessing = 0; 306 | }; 307 | /* End PBXSourcesBuildPhase section */ 308 | 309 | /* Begin PBXVariantGroup section */ 310 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 311 | isa = PBXVariantGroup; 312 | children = ( 313 | 97C146FB1CF9000F007C117D /* Base */, 314 | ); 315 | name = Main.storyboard; 316 | sourceTree = ""; 317 | }; 318 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 319 | isa = PBXVariantGroup; 320 | children = ( 321 | 97C147001CF9000F007C117D /* Base */, 322 | ); 323 | name = LaunchScreen.storyboard; 324 | sourceTree = ""; 325 | }; 326 | /* End PBXVariantGroup section */ 327 | 328 | /* Begin XCBuildConfiguration section */ 329 | 97C147031CF9000F007C117D /* Debug */ = { 330 | isa = XCBuildConfiguration; 331 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 332 | buildSettings = { 333 | ALWAYS_SEARCH_USER_PATHS = NO; 334 | CLANG_ANALYZER_NONNULL = YES; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 336 | CLANG_CXX_LIBRARY = "libc++"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_WARN_BOOL_CONVERSION = YES; 340 | CLANG_WARN_CONSTANT_CONVERSION = YES; 341 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 342 | CLANG_WARN_EMPTY_BODY = YES; 343 | CLANG_WARN_ENUM_CONVERSION = YES; 344 | CLANG_WARN_INFINITE_RECURSION = YES; 345 | CLANG_WARN_INT_CONVERSION = YES; 346 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 347 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 348 | CLANG_WARN_UNREACHABLE_CODE = YES; 349 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 350 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 351 | COPY_PHASE_STRIP = NO; 352 | DEBUG_INFORMATION_FORMAT = dwarf; 353 | ENABLE_STRICT_OBJC_MSGSEND = YES; 354 | ENABLE_TESTABILITY = YES; 355 | GCC_C_LANGUAGE_STANDARD = gnu99; 356 | GCC_DYNAMIC_NO_PIC = NO; 357 | GCC_NO_COMMON_BLOCKS = YES; 358 | GCC_OPTIMIZATION_LEVEL = 0; 359 | GCC_PREPROCESSOR_DEFINITIONS = ( 360 | "DEBUG=1", 361 | "$(inherited)", 362 | ); 363 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 364 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 365 | GCC_WARN_UNDECLARED_SELECTOR = YES; 366 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 367 | GCC_WARN_UNUSED_FUNCTION = YES; 368 | GCC_WARN_UNUSED_VARIABLE = YES; 369 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 370 | MTL_ENABLE_DEBUG_INFO = YES; 371 | ONLY_ACTIVE_ARCH = YES; 372 | SDKROOT = iphoneos; 373 | TARGETED_DEVICE_FAMILY = "1,2"; 374 | }; 375 | name = Debug; 376 | }; 377 | 97C147041CF9000F007C117D /* Release */ = { 378 | isa = XCBuildConfiguration; 379 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 380 | buildSettings = { 381 | ALWAYS_SEARCH_USER_PATHS = NO; 382 | CLANG_ANALYZER_NONNULL = YES; 383 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 384 | CLANG_CXX_LIBRARY = "libc++"; 385 | CLANG_ENABLE_MODULES = YES; 386 | CLANG_ENABLE_OBJC_ARC = YES; 387 | CLANG_WARN_BOOL_CONVERSION = YES; 388 | CLANG_WARN_CONSTANT_CONVERSION = YES; 389 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 390 | CLANG_WARN_EMPTY_BODY = YES; 391 | CLANG_WARN_ENUM_CONVERSION = YES; 392 | CLANG_WARN_INFINITE_RECURSION = YES; 393 | CLANG_WARN_INT_CONVERSION = YES; 394 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 395 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 396 | CLANG_WARN_UNREACHABLE_CODE = YES; 397 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 398 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 399 | COPY_PHASE_STRIP = NO; 400 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 401 | ENABLE_NS_ASSERTIONS = NO; 402 | ENABLE_STRICT_OBJC_MSGSEND = YES; 403 | GCC_C_LANGUAGE_STANDARD = gnu99; 404 | GCC_NO_COMMON_BLOCKS = YES; 405 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 406 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 407 | GCC_WARN_UNDECLARED_SELECTOR = YES; 408 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 409 | GCC_WARN_UNUSED_FUNCTION = YES; 410 | GCC_WARN_UNUSED_VARIABLE = YES; 411 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 412 | MTL_ENABLE_DEBUG_INFO = NO; 413 | SDKROOT = iphoneos; 414 | TARGETED_DEVICE_FAMILY = "1,2"; 415 | VALIDATE_PRODUCT = YES; 416 | }; 417 | name = Release; 418 | }; 419 | 97C147061CF9000F007C117D /* Debug */ = { 420 | isa = XCBuildConfiguration; 421 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 422 | buildSettings = { 423 | ARCHS = arm64; 424 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 425 | DEVELOPMENT_TEAM = NTA2J8MPNL; 426 | ENABLE_BITCODE = NO; 427 | FRAMEWORK_SEARCH_PATHS = ( 428 | "$(inherited)", 429 | "$(PROJECT_DIR)/Flutter", 430 | ); 431 | INFOPLIST_FILE = Runner/Info.plist; 432 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 433 | LIBRARY_SEARCH_PATHS = ( 434 | "$(inherited)", 435 | "$(PROJECT_DIR)/Flutter", 436 | ); 437 | PRODUCT_BUNDLE_IDENTIFIER = com.twofind.stereoExample; 438 | PRODUCT_NAME = "$(TARGET_NAME)"; 439 | }; 440 | name = Debug; 441 | }; 442 | 97C147071CF9000F007C117D /* Release */ = { 443 | isa = XCBuildConfiguration; 444 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 445 | buildSettings = { 446 | ARCHS = arm64; 447 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 448 | DEVELOPMENT_TEAM = NTA2J8MPNL; 449 | ENABLE_BITCODE = NO; 450 | FRAMEWORK_SEARCH_PATHS = ( 451 | "$(inherited)", 452 | "$(PROJECT_DIR)/Flutter", 453 | ); 454 | INFOPLIST_FILE = Runner/Info.plist; 455 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 456 | LIBRARY_SEARCH_PATHS = ( 457 | "$(inherited)", 458 | "$(PROJECT_DIR)/Flutter", 459 | ); 460 | PRODUCT_BUNDLE_IDENTIFIER = com.twofind.stereoExample; 461 | PRODUCT_NAME = "$(TARGET_NAME)"; 462 | }; 463 | name = Release; 464 | }; 465 | /* End XCBuildConfiguration section */ 466 | 467 | /* Begin XCConfigurationList section */ 468 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 469 | isa = XCConfigurationList; 470 | buildConfigurations = ( 471 | 97C147031CF9000F007C117D /* Debug */, 472 | 97C147041CF9000F007C117D /* Release */, 473 | ); 474 | defaultConfigurationIsVisible = 0; 475 | defaultConfigurationName = Release; 476 | }; 477 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 478 | isa = XCConfigurationList; 479 | buildConfigurations = ( 480 | 97C147061CF9000F007C117D /* Debug */, 481 | 97C147071CF9000F007C117D /* Release */, 482 | ); 483 | defaultConfigurationIsVisible = 0; 484 | defaultConfigurationName = Release; 485 | }; 486 | /* End XCConfigurationList section */ 487 | }; 488 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 489 | } 490 | --------------------------------------------------------------------------------