diff --git a/Example/package.json b/Example/package.json index f640bc2..d23b3ad 100644 --- a/Example/package.json +++ b/Example/package.json @@ -6,7 +6,7 @@ "start": "node node_modules/react-native/local-cli/cli.js start" }, "dependencies": { - "react": "15.2.1", + "react": "15.3.2", "react-native": "0.37.0", "react-native-audio-streaming": "file:../" } diff --git a/Example/yarn.lock b/Example/yarn.lock index c7f53f5..fc105e5 100644 --- a/Example/yarn.lock +++ b/Example/yarn.lock @@ -147,7 +147,7 @@ babel-code-frame@^6.16.0: esutils "^2.0.2" js-tokens "^2.0.0" -babel-core@^6.10.4, babel-core@^6.17.0, babel-core@^6.18.0, babel-core@^6.7.2: +babel-core@^6.17.0, babel-core@^6.18.0, babel-core@^6.7.2: version "6.18.2" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.18.2.tgz#d8bb14dd6986fa4f3566a26ceda3964fa0e04e5b" dependencies: @@ -468,7 +468,7 @@ babel-plugin-transform-es3-property-literals@^6.5.0, babel-plugin-transform-es3- dependencies: babel-runtime "^6.0.0" -babel-plugin-transform-flow-strip-types@^6.14.0, babel-plugin-transform-flow-strip-types@^6.5.0, babel-plugin-transform-flow-strip-types@^6.6.5, babel-plugin-transform-flow-strip-types@^6.7.0, babel-plugin-transform-flow-strip-types@^6.8.0: +babel-plugin-transform-flow-strip-types@^6.14.0, babel-plugin-transform-flow-strip-types@^6.5.0, babel-plugin-transform-flow-strip-types@^6.7.0, babel-plugin-transform-flow-strip-types@^6.8.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.18.0.tgz#4d3e642158661e9b40db457c004a30817fa32592" dependencies: @@ -524,7 +524,7 @@ babel-plugin-transform-strict-mode@^6.18.0: babel-runtime "^6.0.0" babel-types "^6.18.0" -babel-polyfill@^6.16.0, babel-polyfill@^6.9.1: +babel-polyfill@^6.16.0: version "6.16.0" resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.16.0.tgz#2d45021df87e26a374b6d4d1a9c65964d17f2422" dependencies: @@ -532,7 +532,7 @@ babel-polyfill@^6.16.0, babel-polyfill@^6.9.1: core-js "^2.4.0" regenerator-runtime "^0.9.5" -babel-preset-es2015-node@^6.1.0, babel-preset-es2015-node@^6.1.1: +babel-preset-es2015-node@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-preset-es2015-node/-/babel-preset-es2015-node-6.1.1.tgz#60b23157024b0cfebf3a63554cb05ee035b4e55f" dependencies: @@ -575,7 +575,7 @@ babel-preset-fbjs@^1.0.0: babel-plugin-transform-object-rest-spread "^6.6.5" object-assign "^4.0.1" -babel-preset-fbjs@^2.0.0, babel-preset-fbjs@^2.1.0: +babel-preset-fbjs@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/babel-preset-fbjs/-/babel-preset-fbjs-2.1.0.tgz#1a8d4cacbac7c5a9194ce3b8475ffab33ed524fb" dependencies: @@ -642,7 +642,7 @@ babel-preset-react-native@^1.9.0: babel-plugin-transform-regenerator "^6.5.0" react-transform-hmr "^1.0.4" -babel-register@^6.16.3, babel-register@^6.18.0, babel-register@^6.6.0: +babel-register@^6.16.3, babel-register@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.18.0.tgz#892e2e03865078dd90ad2c715111ec4449b32a68" dependencies: @@ -685,7 +685,7 @@ babel-traverse@^6.16.0, babel-traverse@^6.18.0: invariant "^2.2.0" lodash "^4.2.0" -babel-types@^6.16.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.6.4, babel-types@^6.8.0, babel-types@^6.9.0: +babel-types@^6.16.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.8.0, babel-types@^6.9.0: version "6.19.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.19.0.tgz#8db2972dbed01f1192a8b602ba1e1e4c516240b9" dependencies: @@ -694,9 +694,9 @@ babel-types@^6.16.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.6. lodash "^4.2.0" to-fast-properties "^1.0.1" -babylon@^6.11.0, babylon@^6.13.0, babylon@^6.8.2: - version "6.14.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.14.0.tgz#c8ba4b69b544b2cd8f3fb96b06614660a49b7128" +babylon@^6.11.0, babylon@^6.13.0: + version "6.14.1" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.14.1.tgz#956275fab72753ad9b3435d7afe58f8bf0a29815" balanced-match@^0.4.1: version "0.4.2" @@ -706,14 +706,14 @@ base62@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/base62/-/base62-1.1.2.tgz#22ced6a49913565bc0b8d9a11563a465c084124c" -base64-js@^0.0.8, base64-js@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" - base64-js@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" +base64-js@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" + base64-url@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/base64-url/-/base64-url-1.2.1.tgz#199fd661702a0e7b7dcae6e0698bb089c52f6d78" @@ -1590,7 +1590,7 @@ fbjs-scripts@^0.7.0: semver "^5.1.0" through2 "^2.0.0" -fbjs@^0.8.1, fbjs@^0.8.3: +fbjs@^0.8.3, fbjs@^0.8.4: version "0.8.6" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.6.tgz#7eb67d6986b2d5007a9b6e92e0e7cb6f75cad290" dependencies: @@ -2732,17 +2732,6 @@ node-fetch@^1.0.1, node-fetch@^1.3.3: encoding "^0.1.11" is-stream "^1.0.1" -node-haste@~2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/node-haste/-/node-haste-2.12.0.tgz#8d4d1cb87f05adcc4207525b789d7dc717f3c1d7" - dependencies: - absolute-path "^0.0.0" - debug "^2.2.0" - denodeify "^1.2.1" - graceful-fs "^4.1.3" - json-stable-stringify "^1.0.1" - sane "^1.3.1" - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -3077,7 +3066,7 @@ react-deep-force-update@^1.0.0: "react-native-audio-streaming@file:../": version "2.2.0" -react-native@^0.37.0: +react-native@0.37.0: version "0.37.0" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.37.0.tgz#d871c752b4d87e46b569fd8be2921d4f9248c87d" dependencies: @@ -3154,78 +3143,6 @@ react-native@^0.37.0: yeoman-environment "1.5.3" yeoman-generator "0.21.2" -react-native@0.31.0: - version "0.31.0" - resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.31.0.tgz#ae37068414b75855c718e465acf70d473e47bccb" - dependencies: - absolute-path "^0.0.0" - art "^0.10.0" - babel-core "^6.10.4" - babel-plugin-external-helpers "^6.8.0" - babel-plugin-syntax-trailing-function-commas "^6.5.0" - babel-plugin-transform-flow-strip-types "^6.6.5" - babel-plugin-transform-object-rest-spread "^6.6.5" - babel-polyfill "^6.9.1" - babel-preset-es2015-node "^6.1.0" - babel-preset-fbjs "^2.0.0" - babel-preset-react-native "^1.9.0" - babel-register "^6.6.0" - babel-types "^6.6.4" - babylon "^6.8.2" - base64-js "^0.0.8" - bser "^1.0.2" - chalk "^1.1.1" - commander "^2.9.0" - connect "^2.8.3" - core-js "^2.2.2" - debug "^2.2.0" - event-target-shim "^1.0.5" - fbjs "^0.8.3" - fbjs-scripts "^0.7.0" - fs-extra "^0.26.2" - glob "^5.0.15" - graceful-fs "^4.1.3" - image-size "^0.3.5" - immutable "~3.7.6" - inquirer "^0.12.0" - joi "^6.6.1" - json-stable-stringify "^1.0.1" - json5 "^0.4.0" - jstransform "^11.0.3" - lodash "^3.10.1" - mime "^1.3.4" - mime-types "2.1.11" - mkdirp "^0.5.1" - module-deps "^3.9.1" - node-fetch "^1.3.3" - node-haste "~2.12.0" - npmlog "^2.0.4" - opn "^3.0.2" - optimist "^0.6.1" - plist "^1.2.0" - progress "^1.1.8" - promise "^7.1.1" - react-clone-referenced-element "^1.0.1" - react-timer-mixin "^0.13.2" - react-transform-hmr "^1.0.4" - rebound "^0.0.13" - regenerator-runtime "^0.9.5" - sane "^1.2.0" - semver "^5.0.3" - source-map "^0.4.4" - stacktrace-parser "^0.1.3" - temp "0.8.3" - uglify-js "^2.6.2" - whatwg-fetch "^1.0.0" - wordwrap "^1.0.0" - worker-farm "^1.3.1" - ws "^1.1.0" - xcode "^0.8.9" - xmldoc "^0.4.0" - yargs "^3.24.0" - yeoman-environment "1.5.3" - yeoman-generator "0.21.2" - react-proxy@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/react-proxy/-/react-proxy-1.1.8.tgz#9dbfd9d927528c3aa9f444e4558c37830ab8c26a" @@ -3244,11 +3161,11 @@ react-transform-hmr@^1.0.4: global "^4.3.0" react-proxy "^1.1.7" -react@15.2.1: - version "15.2.1" - resolved "https://registry.yarnpkg.com/react/-/react-15.2.1.tgz#e458df700bae72900ba32673b7e42e8dbd05a393" +react@15.3.2: + version "15.3.2" + resolved "https://registry.yarnpkg.com/react/-/react-15.3.2.tgz#a7bccd2fee8af126b0317e222c28d1d54528d09e" dependencies: - fbjs "^0.8.1" + fbjs "^0.8.4" loose-envify "^1.1.0" object-assign "^4.1.0" @@ -3463,7 +3380,7 @@ samsam@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" -sane@^1.2.0, sane@^1.3.1, sane@~1.4.1: +sane@~1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/sane/-/sane-1.4.1.tgz#88f763d74040f5f0c256b6163db399bf110ac715" dependencies: @@ -3573,7 +3490,7 @@ source-map-support@^0.4.2: dependencies: source-map "^0.5.3" -source-map@^0.4.2, source-map@^0.4.4: +source-map@^0.4.2: version "0.4.4" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" dependencies: @@ -4072,12 +3989,12 @@ watch@~0.10.0: resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc" whatwg-fetch@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.1.0.tgz#f143957839af3b6fbfbb00f70eb752292fe1cbb6" + version "1.1.1" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.1.1.tgz#ac3c9d39f320c6dce5339969d054ef43dd333319" whatwg-fetch@>=0.10.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.0.tgz#cde428ac2b1dab717c96bc6785feb557619b249e" + version "2.0.1" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.1.tgz#078b9461bbe91cea73cbce8bb122a05f9e92b772" which@^1.2.8, which@^1.2.9: version "1.2.12" diff --git a/android/build.gradle b/android/build.gradle index 07cefe6..af38568 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,12 +2,12 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" + compileSdkVersion 25 + buildToolsVersion "25.0.1" defaultConfig { minSdkVersion 16 - targetSdkVersion 22 + targetSdkVersion 25 versionCode 1 versionName "1.0" ndk { @@ -21,6 +21,6 @@ android { dependencies { compile 'com.facebook.react:react-native:0.20.+' - compile files('libs/aacdecoder-android-0.8.jar') + compile 'com.google.android.exoplayer:exoplayer:r2.4.2' } \ No newline at end of file diff --git a/android/src/main/java/com/audioStreaming/Mode.java b/android/src/main/java/com/audioStreaming/Mode.java index cdbd443..67318f1 100644 --- a/android/src/main/java/com/audioStreaming/Mode.java +++ b/android/src/main/java/com/audioStreaming/Mode.java @@ -3,11 +3,13 @@ public class Mode { public static String CREATED = "CREATED"; public static String CONNECTING = "CONNECTING"; - public static String START_PREPARING = "START_PREPARING"; - public static String PREPARED = "PREPARED"; + public static String PREPARING = "PREPARING"; + public static String READY = "READY"; public static String STARTED = "STARTED"; public static String PLAYING = "PLAYING"; public static String STOPPED = "STOPPED"; + public static String PAUSED = "PAUSED"; + public static String RESUMED = "RESUMED"; public static String COMPLETED = "COMPLETED"; public static String ERROR = "ERROR"; public static String BUFFERING_START = "BUFFERING_START"; @@ -15,4 +17,5 @@ public class Mode { public static String METADATA_UPDATED = "METADATA_UPDATED"; public static String ALBUM_UPDATED = "ALBUM_UPDATED"; public static String DESTROYED = "DESTROYED"; + public static String IDLE = "IDLE"; } diff --git a/android/src/main/java/com/audioStreaming/PhoneListener.java b/android/src/main/java/com/audioStreaming/PhoneListener.java index 6aadf18..76beb74 100644 --- a/android/src/main/java/com/audioStreaming/PhoneListener.java +++ b/android/src/main/java/com/audioStreaming/PhoneListener.java @@ -32,7 +32,7 @@ public void onCallStateChanged(int state, String incomingNumber) { break; case TelephonyManager.CALL_STATE_RINGING: //CALL_STATE_RINGING - if (this.module.getSignal().isPlaying) { + if (this.module.getSignal().isPlaying()) { this.module.stopOncall(); } break; diff --git a/android/src/main/java/com/audioStreaming/ReactNativeAudioStreamingModule.java b/android/src/main/java/com/audioStreaming/ReactNativeAudioStreamingModule.java index 00921ef..e46641a 100644 --- a/android/src/main/java/com/audioStreaming/ReactNativeAudioStreamingModule.java +++ b/android/src/main/java/com/audioStreaming/ReactNativeAudioStreamingModule.java @@ -16,55 +16,57 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import javax.annotation.Nullable; +import android.app.Activity; public class ReactNativeAudioStreamingModule extends ReactContextBaseJavaModule implements ServiceConnection { - + public static final String SHOULD_SHOW_NOTIFICATION = "showInAndroidNotifications"; private ReactApplicationContext context; - + private Class clsActivity; private static Signal signal; private Intent bindIntent; - private String streamingURL; private boolean shouldShowNotification; - + + public ReactNativeAudioStreamingModule(ReactApplicationContext reactContext) { super(reactContext); this.context = reactContext; } - + public ReactApplicationContext getReactApplicationContextModule() { return this.context; } - + public Class getClassActivity() { - if (this.clsActivity == null) { - this.clsActivity = getCurrentActivity().getClass(); + Activity activity = getCurrentActivity(); + if (this.clsActivity == null && activity != null) { + this.clsActivity = activity.getClass(); } return this.clsActivity; } - + public void stopOncall() { this.signal.stop(); } - + public Signal getSignal() { return signal; } - + public void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) { this.context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params); } - + @Override public String getName() { return "ReactNativeAudioStreaming"; } - + @Override public void initialize() { super.initialize(); - + try { bindIntent = new Intent(this.context, Signal.class); this.context.bindService(bindIntent, this, Context.BIND_AUTO_CREATE); @@ -72,54 +74,63 @@ public void sendEvent(ReactContext reactContext, String eventName, @Nullable Wri Log.e("ERROR", e.getMessage()); } } - + @Override public void onServiceConnected(ComponentName className, IBinder service) { signal = ((Signal.RadioBinder) service).getService(); signal.setData(this.context, this); WritableMap params = Arguments.createMap(); sendEvent(this.getReactApplicationContextModule(), "streamingOpen", params); } - + @Override public void onServiceDisconnected(ComponentName className) { signal = null; } - + @ReactMethod public void play(String streamingURL, ReadableMap options) { - this.streamingURL = streamingURL; - this.shouldShowNotification = - options.hasKey(SHOULD_SHOW_NOTIFICATION) && options.getBoolean(SHOULD_SHOW_NOTIFICATION); - signal.setURLStreaming(streamingURL); // URL of MP3 or AAC stream - playInternal(); + this.shouldShowNotification = options.hasKey(SHOULD_SHOW_NOTIFICATION) && options.getBoolean(SHOULD_SHOW_NOTIFICATION); + playInternal(streamingURL); } - - private void playInternal() { - signal.play(); + + private void playInternal(String streamingURL) { + signal.play(streamingURL); + if (shouldShowNotification) { signal.showNotification(); } } - + @ReactMethod public void stop() { signal.stop(); } - + @ReactMethod public void pause() { // Not implemented on aac this.stop(); } - + @ReactMethod public void resume() { // Not implemented on aac - playInternal(); + signal.resume(); } - + @ReactMethod public void destroyNotification() { signal.exitNotification(); } - + + @ReactMethod public void seekToTime(int seconds) { + signal.seekTo(seconds * 1000); + } + @ReactMethod public void getStatus(Callback callback) { WritableMap state = Arguments.createMap(); - state.putString("status", signal != null && signal.isPlaying ? Mode.PLAYING : Mode.STOPPED); + state.putDouble("duration", signal.getDuration()); + state.putDouble("progress", signal.getCurrentPosition()); + state.putString("status", signal != null && signal.isPlaying() ? Mode.PLAYING : Mode.STOPPED); + state.putString("url", signal.getStreamingURL()); callback.invoke(null, state); } + + @ReactMethod public void setCurrentPlaybackRate(float speed) { + signal.setPlaybackRate(speed); + } } diff --git a/android/src/main/java/com/audioStreaming/ReactNativeAudioStreamingPackage.java b/android/src/main/java/com/audioStreaming/ReactNativeAudioStreamingPackage.java index 9536ba0..ba3d032 100644 --- a/android/src/main/java/com/audioStreaming/ReactNativeAudioStreamingPackage.java +++ b/android/src/main/java/com/audioStreaming/ReactNativeAudioStreamingPackage.java @@ -22,7 +22,7 @@ public List createNativeModules(ReactApplicationContext reactConte return modules; } - @Override + // @Override public List> createJSModules() { return Collections.emptyList(); } diff --git a/android/src/main/java/com/audioStreaming/Signal.java b/android/src/main/java/com/audioStreaming/Signal.java index 7bf4262..3685658 100644 --- a/android/src/main/java/com/audioStreaming/Signal.java +++ b/android/src/main/java/com/audioStreaming/Signal.java @@ -6,12 +6,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.media.AudioTrack; -import android.media.MediaPlayer; -import android.media.MediaPlayer.OnCompletionListener; -import android.media.MediaPlayer.OnErrorListener; -import android.media.MediaPlayer.OnInfoListener; -import android.media.MediaPlayer.OnPreparedListener; +import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Binder; @@ -21,17 +16,48 @@ import android.support.v4.app.TaskStackBuilder; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; +import android.util.Log; import android.widget.RemoteViews; - -import com.spoledge.aacdecoder.MultiPlayer; -import com.spoledge.aacdecoder.PlayerCallback; - -public class Signal extends Service implements OnErrorListener, - OnCompletionListener, - OnPreparedListener, - OnInfoListener, - PlayerCallback { - +import android.os.Build; +import android.net.Uri; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.content.pm.ApplicationInfo; + +import com.facebook.infer.annotation.Assertions; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.metadata.id3.ApicFrame; +import com.google.android.exoplayer2.metadata.id3.GeobFrame; +import com.google.android.exoplayer2.metadata.id3.Id3Frame; +import com.google.android.exoplayer2.metadata.id3.PrivFrame; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.metadata.Metadata; + +import java.io.IOException; +import java.util.List; + +public class Signal extends Service implements ExoPlayer.EventListener, MetadataRenderer.Output, ExtractorMediaSource.EventListener { + private static final String TAG = "ReactNative"; // Notification private Class clsActivity; @@ -39,44 +65,59 @@ public class Signal extends Service implements OnErrorListener, private NotificationCompat.Builder notifyBuilder; private NotificationManager notifyManager = null; public static RemoteViews remoteViews; - private MultiPlayer aacPlayer; - private static final int AAC_BUFFER_CAPACITY_MS = 2500; - private static final int AAC_DECODER_CAPACITY_MS = 700; + // Player + private SimpleExoPlayer player = null; public static final String BROADCAST_PLAYBACK_STOP = "stop", BROADCAST_PLAYBACK_PLAY = "pause", BROADCAST_EXIT = "exit"; - private final Handler handler = new Handler(); private final IBinder binder = new RadioBinder(); private final SignalReceiver receiver = new SignalReceiver(this); private Context context; private String streamingURL; - public boolean isPlaying = false; - private boolean isPreparingStarted = false; private EventsReceiver eventsReceiver; private ReactNativeAudioStreamingModule module; private TelephonyManager phoneManager; private PhoneListener phoneStateListener; + @Override + public void onCreate() { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(BROADCAST_PLAYBACK_STOP); + intentFilter.addAction(BROADCAST_PLAYBACK_PLAY); + intentFilter.addAction(BROADCAST_EXIT); + registerReceiver(this.receiver, intentFilter); + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + + } + public void setData(Context context, ReactNativeAudioStreamingModule module) { this.context = context; this.clsActivity = module.getClassActivity(); this.module = module; - + this.eventsReceiver = new EventsReceiver(this.module); - - + registerReceiver(this.eventsReceiver, new IntentFilter(Mode.CREATED)); + registerReceiver(this.eventsReceiver, new IntentFilter(Mode.IDLE)); registerReceiver(this.eventsReceiver, new IntentFilter(Mode.DESTROYED)); registerReceiver(this.eventsReceiver, new IntentFilter(Mode.STARTED)); registerReceiver(this.eventsReceiver, new IntentFilter(Mode.CONNECTING)); - registerReceiver(this.eventsReceiver, new IntentFilter(Mode.START_PREPARING)); - registerReceiver(this.eventsReceiver, new IntentFilter(Mode.PREPARED)); registerReceiver(this.eventsReceiver, new IntentFilter(Mode.PLAYING)); + registerReceiver(this.eventsReceiver, new IntentFilter(Mode.READY)); registerReceiver(this.eventsReceiver, new IntentFilter(Mode.STOPPED)); + registerReceiver(this.eventsReceiver, new IntentFilter(Mode.PAUSED)); registerReceiver(this.eventsReceiver, new IntentFilter(Mode.COMPLETED)); registerReceiver(this.eventsReceiver, new IntentFilter(Mode.ERROR)); registerReceiver(this.eventsReceiver, new IntentFilter(Mode.BUFFERING_START)); @@ -84,122 +125,266 @@ public void setData(Context context, ReactNativeAudioStreamingModule module) { registerReceiver(this.eventsReceiver, new IntentFilter(Mode.METADATA_UPDATED)); registerReceiver(this.eventsReceiver, new IntentFilter(Mode.ALBUM_UPDATED)); - this.phoneStateListener = new PhoneListener(this.module); this.phoneManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); if (this.phoneManager != null) { this.phoneManager.listen(this.phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } - - } @Override - public void onCreate() { - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(BROADCAST_PLAYBACK_STOP); - intentFilter.addAction(BROADCAST_PLAYBACK_PLAY); - intentFilter.addAction(BROADCAST_EXIT); - registerReceiver(this.receiver, intentFilter); + public void onLoadingChanged(boolean isLoading) { + } - try { - this.aacPlayer = new MultiPlayer(this, AAC_BUFFER_CAPACITY_MS, AAC_DECODER_CAPACITY_MS); - } catch (UnsatisfiedLinkError e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + Log.d("onPlayerStateChanged", ""+playbackState); + switch (playbackState) { + case ExoPlayer.STATE_IDLE: + sendBroadcast(new Intent(Mode.IDLE)); + break; + case ExoPlayer.STATE_BUFFERING: - this.notifyManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - try { - java.net.URL.setURLStreamHandlerFactory(new java.net.URLStreamHandlerFactory() { - public java.net.URLStreamHandler createURLStreamHandler(String protocol) { - if ("icy".equals(protocol)) { - return new com.spoledge.aacdecoder.IcyURLStreamHandler(); - } - return null; + sendBroadcast(new Intent(Mode.BUFFERING_START)); + break; + case ExoPlayer.STATE_READY: + if (this.player != null && this.player.getPlayWhenReady()) { + sendBroadcast(new Intent(Mode.PLAYING)); + } else { + sendBroadcast(new Intent(Mode.READY)); } - }); - } catch (Throwable t) { - + break; + case ExoPlayer.STATE_ENDED: + sendBroadcast(new Intent(Mode.STOPPED)); + break; } + } - sendBroadcast(new Intent(Mode.CREATED)); + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) {} + + + @Override + public void onPlayerError(ExoPlaybackException error) { + Log.d(TAG, error.getMessage()); + sendBroadcast(new Intent(Mode.ERROR)); } + @Override + public void onPositionDiscontinuity() { - public void setURLStreaming(String streamingURL) { - this.streamingURL = streamingURL; } - public void play() { - if (isConnected()) { - this.prepare(); - } else { - sendBroadcast(new Intent(Mode.STOPPED)); + private static String getDefaultUserAgent() { + StringBuilder result = new StringBuilder(64); + result.append("Dalvik/"); + result.append(System.getProperty("java.vm.version")); // such as 1.1.0 + result.append(" (Linux; U; Android "); + + String version = Build.VERSION.RELEASE; // "1.0" or "3.4b5" + result.append(version.length() > 0 ? version : "1.0"); + + // add the model for the release build + if ("REL".equals(Build.VERSION.CODENAME)) { + String model = Build.MODEL; + if (model.length() > 0) { + result.append("; "); + result.append(model); + } } - - this.isPlaying = true; + String id = Build.ID; // "MASTER" or "M4-rc20" + if (id.length() > 0) { + result.append(" Build/"); + result.append(id); + } + result.append(")"); + return result.toString(); } - - public void stop() { - this.isPreparingStarted = false; - - if (this.isPlaying) { - this.isPlaying = false; - this.aacPlayer.stop(); + + /** + * Player controls + */ + + public void play(String url) { + if (player != null ) { + player.setPlayWhenReady(false); + player.stop(); + player.seekTo(0); } - + + boolean playWhenReady = true; // TODO Allow user to customize this + this.streamingURL = url; + + // Create player + Handler mainHandler = new Handler(); + TrackSelector trackSelector = new DefaultTrackSelector(); + LoadControl loadControl = new DefaultLoadControl(); + this.player = ExoPlayerFactory.newSimpleInstance(this.getApplicationContext(), trackSelector, loadControl); + + // Create source + ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); + DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this.getApplication(), getDefaultUserAgent(), bandwidthMeter); + MediaSource audioSource = new ExtractorMediaSource(Uri.parse(this.streamingURL), dataSourceFactory, extractorsFactory, mainHandler, this); + + // Start preparing audio + player.prepare(audioSource); + player.addListener(this); + player.setPlayWhenReady(playWhenReady); + } + + public void start() { + Assertions.assertNotNull(player); + player.setPlayWhenReady(true); + } + + public void pause() { + Assertions.assertNotNull(player); + player.setPlayWhenReady(false); + sendBroadcast(new Intent(Mode.STOPPED)); + } + + public void resume() { + Assertions.assertNotNull(player); + player.setPlayWhenReady(true); + } + + public void stop() { + Assertions.assertNotNull(player); + player.setPlayWhenReady(false); sendBroadcast(new Intent(Mode.STOPPED)); } + + public boolean isPlaying() { + //Assertions.assertNotNull(player); + return player != null && player.getPlayWhenReady() && player.getPlaybackState() != ExoPlayer.STATE_ENDED; + } + + public long getDuration() { + //Assertions.assertNotNull(player); + return player != null ? player.getDuration() : new Long(0); + } + + public long getCurrentPosition() { + //Assertions.assertNotNull(player); + return player != null ? player.getCurrentPosition() : new Long(0); + } + + public int getBufferPercentage() { + Assertions.assertNotNull(player); + return player.getBufferedPercentage(); + } + + public void seekTo(long timeMillis) { + Assertions.assertNotNull(player); + player.seekTo(timeMillis); + } + + public boolean isConnected() { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo netInfo = cm.getActiveNetworkInfo(); + return netInfo != null && netInfo.isConnectedOrConnecting(); + } + public void setPlaybackRate(float speed) { + PlaybackParameters pp = new PlaybackParameters(speed, 1); + player.setPlaybackParameters(pp); + } + + /** + * Meta data information + */ + + @Override + public void onMetadata(Metadata metadata) { + + } + + /** + * Notification control + */ + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return Service.START_NOT_STICKY; + } + + // Notification + private PendingIntent makePendingIntent(String broadcast) { + Intent intent = new Intent(broadcast); + return PendingIntent.getBroadcast(this.context, 0, intent, 0); + } + public NotificationManager getNotifyManager() { return notifyManager; } - + public class RadioBinder extends Binder { public Signal getService() { return Signal.this; } } - + + public String getAppTitle() { + ApplicationInfo applicationInfo = context.getApplicationInfo(); + int stringId = applicationInfo.labelRes; + String title = stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : context.getString(stringId); + return title; + } + public void showNotification() { - remoteViews = new RemoteViews(context.getPackageName(), R.layout.streaming_notification_player); + if (this.clsActivity == null) { + this.clsActivity = this.module.getClassActivity(); + } + Resources res = context.getResources(); + String packageName = context.getPackageName(); + int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName); + int largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName); + Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId); + remoteViews = new RemoteViews(packageName, R.layout.streaming_notification_player); notifyBuilder = new NotificationCompat.Builder(this.context) - .setSmallIcon(android.R.drawable.ic_lock_silent_mode_off) // TODO Use app icon instead - .setContentText("") - .setOngoing(true) - .setContent(remoteViews); - + .setContent(remoteViews) + .setSmallIcon(smallIconResId) + .setLargeIcon(largeIconBitmap) + .setContentTitle(this.getAppTitle()) + .setContentText("Playing an audio file") + .setOngoing(true) + ; + Intent resultIntent = new Intent(this.context, this.clsActivity); resultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); resultIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - + TaskStackBuilder stackBuilder = TaskStackBuilder.create(this.context); stackBuilder.addParentStack(this.clsActivity); stackBuilder.addNextIntent(resultIntent); - + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, - PendingIntent.FLAG_UPDATE_CURRENT); - + PendingIntent.FLAG_UPDATE_CURRENT); + notifyBuilder.setContentIntent(resultPendingIntent); + + remoteViews.setTextViewText(R.id.title, this.getAppTitle()); + remoteViews.setTextViewText(R.id.subtitle, "Playing an audio file"); + remoteViews.setImageViewResource(R.id.streaming_icon, largeIconResId); remoteViews.setOnClickPendingIntent(R.id.btn_streaming_notification_play, makePendingIntent(BROADCAST_PLAYBACK_PLAY)); remoteViews.setOnClickPendingIntent(R.id.btn_streaming_notification_stop, makePendingIntent(BROADCAST_EXIT)); notifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notifyManager.notify(NOTIFY_ME_ID, notifyBuilder.build()); } - - private PendingIntent makePendingIntent(String broadcast) { - Intent intent = new Intent(broadcast); - return PendingIntent.getBroadcast(this.context, 0, intent, 0); - } - + public void clearNotification() { - if (notifyManager != null) + if (notifyManager != null) { notifyManager.cancel(NOTIFY_ME_ID); + } } - + public void exitNotification() { notifyManager.cancelAll(); clearNotification(); @@ -207,147 +392,12 @@ public void exitNotification() { notifyManager = null; } - public boolean isConnected() { - ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo netInfo = cm.getActiveNetworkInfo(); - if (netInfo != null && netInfo.isConnectedOrConnecting()) { - return true; - } - return false; - } - - public void prepare() { - /* ------Station- buffering-------- */ - this.isPreparingStarted = true; - sendBroadcast(new Intent(Mode.START_PREPARING)); - - try { - this.aacPlayer.playAsync(this.streamingURL); - } catch (Exception e) { - e.printStackTrace(); - stop(); - } - } - - @Override - public IBinder onBind(Intent intent) { - return binder; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (this.isPlaying) { - sendBroadcast(new Intent(Mode.PLAYING)); - } else if (this.isPreparingStarted) { - sendBroadcast(new Intent(Mode.START_PREPARING)); - } else { - sendBroadcast(new Intent(Mode.STARTED)); - } - - return Service.START_NOT_STICKY; + public String getStreamingURL() { + return this.streamingURL; } @Override - public void onPrepared(MediaPlayer _mediaPlayer) { - this.isPreparingStarted = false; - sendBroadcast(new Intent(Mode.PREPARED)); + public void onLoadError(IOException error) { + Log.e(TAG, error.getMessage()); } - - @Override - public void onCompletion(MediaPlayer mediaPlayer) { - this.isPlaying = false; - this.aacPlayer.stop(); - sendBroadcast(new Intent(Mode.COMPLETED)); - } - - @Override - public boolean onInfo(MediaPlayer mp, int what, int extra) { - if (what == 701) { - this.isPlaying = false; - sendBroadcast(new Intent(Mode.BUFFERING_START)); - } else if (what == 702) { - this.isPlaying = true; - sendBroadcast(new Intent(Mode.BUFFERING_END)); - } - return false; - } - - @Override - public boolean onError(MediaPlayer mp, int what, int extra) { - switch (what) { - case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK: - //Log.v("ERROR", "MEDIA ERROR NOT VALID FOR PROGRESSIVE PLAYBACK " + extra); - break; - case MediaPlayer.MEDIA_ERROR_SERVER_DIED: - //Log.v("ERROR", "MEDIA ERROR SERVER DIED " + extra); - break; - case MediaPlayer.MEDIA_ERROR_UNKNOWN: - //Log.v("ERROR", "MEDIA ERROR UNKNOWN " + extra); - break; - } - sendBroadcast(new Intent(Mode.ERROR)); - return false; - } - - @Override - public void playerStarted() { - // TODO - } - - @Override - public void playerPCMFeedBuffer(boolean isPlaying, int bufSizeMs, int bufCapacityMs) { - if (isPlaying) { - this.isPreparingStarted = false; - if (bufSizeMs < 500) { - this.isPlaying = false; - sendBroadcast(new Intent(Mode.BUFFERING_START)); - //buffering - } else { - this.isPlaying = true; - sendBroadcast(new Intent(Mode.PLAYING)); - //playing - } - } else { - //buffering - this.isPlaying = false; - sendBroadcast(new Intent(Mode.BUFFERING_START)); - } - } - - @Override - public void playerException(final Throwable t) { - this.isPlaying = false; - this.isPreparingStarted = false; - sendBroadcast(new Intent(Mode.ERROR)); - // TODO - } - - @Override - public void playerMetadata(final String key, final String value) { - Intent metaIntent = new Intent(Mode.METADATA_UPDATED); - metaIntent.putExtra("key", key); - metaIntent.putExtra("value", value); - sendBroadcast(metaIntent); - - if (key != null && key.equals("StreamTitle")) { - remoteViews.setTextViewText(R.id.song_name_notification, value); - notifyBuilder.setContent(remoteViews); - notifyManager.notify(NOTIFY_ME_ID, notifyBuilder.build()); - } - } - - @Override - public void playerAudioTrackCreated(AudioTrack atrack) { - // TODO - } - - @Override - public void playerStopped(int perf) { - this.isPlaying = false; - this.isPreparingStarted = false; - sendBroadcast(new Intent(Mode.STOPPED)); - // TODO - } - - } diff --git a/android/src/main/java/com/audioStreaming/SignalReceiver.java b/android/src/main/java/com/audioStreaming/SignalReceiver.java index b21b567..0fa76a7 100644 --- a/android/src/main/java/com/audioStreaming/SignalReceiver.java +++ b/android/src/main/java/com/audioStreaming/SignalReceiver.java @@ -3,6 +3,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.util.Log; class SignalReceiver extends BroadcastReceiver { private Signal signal; @@ -15,11 +16,12 @@ public SignalReceiver(Signal signal) { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); + if (action.equals(Signal.BROADCAST_PLAYBACK_PLAY)) { - if (!this.signal.isPlaying) { - this.signal.play(); + if (!this.signal.isPlaying()) { + this.signal.resume(); } else { - this.signal.stop(); + this.signal.pause(); } } else if (action.equals(Signal.BROADCAST_EXIT)) { this.signal.getNotifyManager().cancelAll(); diff --git a/android/src/main/res/layout/streaming_notification_player.xml b/android/src/main/res/layout/streaming_notification_player.xml index 0a05a34..bcda5f3 100644 --- a/android/src/main/res/layout/streaming_notification_player.xml +++ b/android/src/main/res/layout/streaming_notification_player.xml @@ -1,49 +1,76 @@ - + + - - - - - + + + - - + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/subtitle" + android:text="BLABLA" + android:textColor="#bbbbbb" + android:layout_marginTop="25dp" + android:textIsSelectable="false" /> + + + + + + diff --git a/index.js b/index.js index 4b11305..151d8fa 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ import { ActivityIndicator, Platform } from 'react-native'; - +import PropTypes from 'prop-types'; const { ReactNativeAudioStreaming } = NativeModules; // Possibles states @@ -149,7 +149,7 @@ const styles = StyleSheet.create({ }); Player.propTypes = { - url: React.PropTypes.string.isRequired + url: PropTypes.string.isRequired }; export { Player, ReactNativeAudioStreaming } diff --git a/ios/ReactNativeAudioStreaming.m b/ios/ReactNativeAudioStreaming.m index 046af54..ee68425 100644 --- a/ios/ReactNativeAudioStreaming.m +++ b/ios/ReactNativeAudioStreaming.m @@ -40,19 +40,20 @@ -(void) tick:(NSTimer*)timer if (!self.audioPlayer) { return; } - + if (self.audioPlayer.currentlyPlayingQueueItemId != nil && self.audioPlayer.state == STKAudioPlayerStatePlaying) { NSNumber *progress = [NSNumber numberWithFloat:self.audioPlayer.progress]; NSNumber *duration = [NSNumber numberWithFloat:self.audioPlayer.duration]; NSString *url = [NSString stringWithString:self.audioPlayer.currentlyPlayingQueueItemId]; - + [self.bridge.eventDispatcher sendDeviceEventWithName:@"AudioBridgeEvent" body:@{ - @"status": @"STREAMING", - @"progress": progress, - @"duration": duration, - @"url": url, - }]; - } + @"status": @"STREAMING", + @"progress": progress, + @"duration": duration, + @"url": url, + }]; + [self setNowPlayingInfo:true]; + } } @@ -71,22 +72,22 @@ - (void)dealloc if (!self.audioPlayer) { return; } - + [self activate]; - + if (self.audioPlayer.state == STKAudioPlayerStatePaused && [self.lastUrlString isEqualToString:streamUrl]) { [self.audioPlayer resume]; } else { [self.audioPlayer play:streamUrl]; } - + self.lastUrlString = streamUrl; self.showNowPlayingInfo = false; - + if ([options objectForKey:@"showIniOSMediaCenter"]) { self.showNowPlayingInfo = [[options objectForKey:@"showIniOSMediaCenter"] boolValue]; } - + if (self.showNowPlayingInfo) { //unregister any existing registrations [self unregisterAudioInterruptionNotifications]; @@ -95,7 +96,7 @@ - (void)dealloc [self registerAudioInterruptionNotifications]; [self registerRemoteControlEvents]; } - + [self setNowPlayingInfo:true]; } @@ -104,7 +105,7 @@ - (void)dealloc if (!self.audioPlayer) { return; } - + [self.audioPlayer seekToTime:seconds]; } @@ -113,9 +114,9 @@ - (void)dealloc if (!self.audioPlayer) { return; } - + double newtime = self.audioPlayer.progress + seconds; - + if (self.audioPlayer.duration < newtime) { [self.audioPlayer stop]; [self setNowPlayingInfo:false]; @@ -129,9 +130,9 @@ - (void)dealloc if (!self.audioPlayer) { return; } - + double newtime = self.audioPlayer.progress - seconds; - + if (newtime < 0) { [self.audioPlayer seekToTime:0.0]; } else { @@ -177,7 +178,7 @@ - (void)dealloc NSString *status = @"STOPPED"; NSNumber *duration = [NSNumber numberWithFloat:self.audioPlayer.duration]; NSNumber *progress = [NSNumber numberWithFloat:self.audioPlayer.progress]; - + if (!self.audioPlayer) { status = @"ERROR"; } else if ([self.audioPlayer state] == STKAudioPlayerStatePlaying) { @@ -187,7 +188,7 @@ - (void)dealloc } else if ([self.audioPlayer state] == STKAudioPlayerStateBuffering) { status = @"BUFFERING"; } - + callback(@[[NSNull null], @{@"status": status, @"progress": progress, @"duration": duration, @"url": self.lastUrlString}]); } @@ -215,13 +216,13 @@ - (void)audioPlayer:(STKAudioPlayer *)player unexpectedError:(STKAudioPlayerErro - (void)audioPlayer:(STKAudioPlayer *)audioPlayer didReadStreamMetadata:(NSDictionary *)dictionary { NSLog(@"AudioPlayer SONG NAME %@", dictionary[@"StreamTitle"]); - + self.currentSong = dictionary[@"StreamTitle"]; [self.bridge.eventDispatcher sendDeviceEventWithName:@"AudioBridgeEvent" body:@{ - @"status": @"METADATA_UPDATED", - @"key": @"StreamTitle", - @"value": dictionary[@"StreamTitle"] - }]; + @"status": @"METADATA_UPDATED", + @"key": @"StreamTitle", + @"value": dictionary[@"StreamTitle"] + }]; [self setNowPlayingInfo:true]; } @@ -229,33 +230,33 @@ - (void)audioPlayer:(STKAudioPlayer *)player stateChanged:(STKAudioPlayerState)s { NSNumber *duration = [NSNumber numberWithFloat:self.audioPlayer.duration]; NSNumber *progress = [NSNumber numberWithFloat:self.audioPlayer.progress]; - + switch (state) { case STKAudioPlayerStatePlaying: [self.bridge.eventDispatcher sendDeviceEventWithName:@"AudioBridgeEvent" body:@{@"status": @"PLAYING", @"progress": progress, @"duration": duration, @"url": self.lastUrlString}]; break; - + case STKAudioPlayerStatePaused: [self.bridge.eventDispatcher sendDeviceEventWithName:@"AudioBridgeEvent" body:@{@"status": @"PAUSED", @"progress": progress, @"duration": duration, @"url": self.lastUrlString}]; break; - + case STKAudioPlayerStateStopped: [self.bridge.eventDispatcher sendDeviceEventWithName:@"AudioBridgeEvent" body:@{@"status": @"STOPPED", @"progress": progress, @"duration": duration, @"url": self.lastUrlString}]; break; - + case STKAudioPlayerStateBuffering: [self.bridge.eventDispatcher sendDeviceEventWithName:@"AudioBridgeEvent" body:@{@"status": @"BUFFERING"}]; break; - + case STKAudioPlayerStateError: [self.bridge.eventDispatcher sendDeviceEventWithName:@"AudioBridgeEvent" body:@{@"status": @"ERROR"}]; break; - + default: break; } @@ -267,10 +268,10 @@ - (void)audioPlayer:(STKAudioPlayer *)player stateChanged:(STKAudioPlayerState)s - (void)activate { NSError *categoryError = nil; - + [[AVAudioSession sharedInstance] setActive:YES error:&categoryError]; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&categoryError]; - + if (categoryError) { NSLog(@"Error setting category! %@", [categoryError description]); } @@ -279,9 +280,9 @@ - (void)activate - (void)deactivate { NSError *categoryError = nil; - + [[AVAudioSession sharedInstance] setActive:NO error:&categoryError]; - + if (categoryError) { NSLog(@"Error setting category! %@", [categoryError description]); } @@ -291,10 +292,10 @@ - (void)setSharedAudioSessionCategory { NSError *categoryError = nil; self.isPlayingWithOthers = [[AVAudioSession sharedInstance] isOtherAudioPlaying]; - + [[AVAudioSession sharedInstance] setActive:NO error:&categoryError]; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:&categoryError]; - + if (categoryError) { NSLog(@"Error setting category! %@", [categoryError description]); } @@ -304,48 +305,47 @@ - (void)registerAudioInterruptionNotifications { // Register for audio interrupt notifications [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(onAudioInterruption:) + selector:@selector(onAudioInterruption:) name:AVAudioSessionInterruptionNotification - object:nil]; + object:nil]; // Register for route change notifications [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(onRouteChangeInterruption:) + selector:@selector(onRouteChangeInterruption:) name:AVAudioSessionRouteChangeNotification - object:nil]; + object:nil]; } - (void)unregisterAudioInterruptionNotifications { [[NSNotificationCenter defaultCenter] removeObserver:self - name:AVAudioSessionRouteChangeNotification - object:nil]; + name:AVAudioSessionRouteChangeNotification + object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self - name:AVAudioSessionInterruptionNotification - object:nil]; + name:AVAudioSessionInterruptionNotification + object:nil]; } - (void)onAudioInterruption:(NSNotification *)notification { // Get the user info dictionary NSDictionary *interruptionDict = notification.userInfo; - + // Get the AVAudioSessionInterruptionTypeKey enum from the dictionary NSInteger interuptionType = [[interruptionDict valueForKey:AVAudioSessionInterruptionTypeKey] integerValue]; - - // Decide what to do based on interruption type - switch (interuptionType) - { + + // Decide what to do based on interruption type + switch (interuptionType) { case AVAudioSessionInterruptionTypeBegan: NSLog(@"Audio Session Interruption case started."); [self.audioPlayer pause]; break; - + case AVAudioSessionInterruptionTypeEnded: NSLog(@"Audio Session Interruption case ended."); self.isPlayingWithOthers = [[AVAudioSession sharedInstance] isOtherAudioPlaying]; (self.isPlayingWithOthers) ? [self.audioPlayer stop] : [self.audioPlayer resume]; break; - + default: NSLog(@"Audio Session Interruption Notification case default."); break; @@ -354,41 +354,40 @@ - (void)onAudioInterruption:(NSNotification *)notification - (void)onRouteChangeInterruption:(NSNotification *)notification { - + NSDictionary *interruptionDict = notification.userInfo; NSInteger routeChangeReason = [[interruptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue]; - - switch (routeChangeReason) - { + + switch (routeChangeReason) { case AVAudioSessionRouteChangeReasonUnknown: NSLog(@"routeChangeReason : AVAudioSessionRouteChangeReasonUnknown"); break; - + case AVAudioSessionRouteChangeReasonNewDeviceAvailable: // A user action (such as plugging in a headset) has made a preferred audio route available. NSLog(@"routeChangeReason : AVAudioSessionRouteChangeReasonNewDeviceAvailable"); break; - + case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: // The previous audio output path is no longer available. [self.audioPlayer stop]; break; - + case AVAudioSessionRouteChangeReasonCategoryChange: // The category of the session object changed. Also used when the session is first activated. NSLog(@"routeChangeReason : AVAudioSessionRouteChangeReasonCategoryChange"); //AVAudioSessionRouteChangeReasonCategoryChange break; - + case AVAudioSessionRouteChangeReasonOverride: // The output route was overridden by the app. NSLog(@"routeChangeReason : AVAudioSessionRouteChangeReasonOverride"); break; - + case AVAudioSessionRouteChangeReasonWakeFromSleep: // The route changed when the device woke up from sleep. NSLog(@"routeChangeReason : AVAudioSessionRouteChangeReasonWakeFromSleep"); break; - + case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory: // The route changed because no suitable route is now available for the specified category. NSLog(@"routeChangeReason : AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory"); @@ -408,6 +407,40 @@ - (void)registerRemoteControlEvents commandCenter.stopCommand.enabled = NO; commandCenter.nextTrackCommand.enabled = NO; commandCenter.previousTrackCommand.enabled = NO; + + MPSkipIntervalCommand *skipBackwardIntervalCommand = [commandCenter skipBackwardCommand]; + [skipBackwardIntervalCommand setEnabled:YES]; + [skipBackwardIntervalCommand addTarget:self action:@selector(skipBackwardEvent:)]; + skipBackwardIntervalCommand.preferredIntervals = @[@(30)]; // Set your own interval + + MPSkipIntervalCommand *skipForwardIntervalCommand = [commandCenter skipForwardCommand]; + skipForwardIntervalCommand.preferredIntervals = @[@(30)]; // Max 99 + [skipForwardIntervalCommand setEnabled:YES]; + [skipForwardIntervalCommand addTarget:self action:@selector(skipForwardEvent:)]; + + [commandCenter.changePlaybackPositionCommand addTarget:self action: @selector( onChangePlaybackPositionCommand: )]; +} + +- (MPRemoteCommandHandlerStatus)onChangePlaybackPositionCommand:(MPChangePlaybackPositionCommandEvent *)event +{ + // change position + [self seekToTime:event.positionTime]; + // update MPNowPlayingInfoPropertyElapsedPlaybackTime + //[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:songInfo]; + + return MPRemoteCommandHandlerStatusSuccess; +} + +-(void)skipBackwardEvent: (MPSkipIntervalCommandEvent *)skipEvent +{ + [self goBack :30]; + NSLog(@"Skip backward by %f", skipEvent.interval); +} + +-(void)skipForwardEvent: (MPSkipIntervalCommandEvent *)skipEvent +{ + [self goForward :30]; + NSLog(@"Skip forward by %f", skipEvent.interval); } - (MPRemoteCommandHandlerStatus)didReceivePlayCommand:(MPRemoteCommand *)event @@ -436,13 +469,15 @@ - (void)setNowPlayingInfo:(bool)isPlaying if (self.showNowPlayingInfo) { // TODO Get artwork from stream // MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc]initWithImage:[UIImage imageNamed:@"webradio1"]]; - + NSString* appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]; NSDictionary *nowPlayingInfo = [NSDictionary dictionaryWithObjectsAndKeys: - self.currentSong ? self.currentSong : @"", MPMediaItemPropertyAlbumTitle, - @"", MPMediaItemPropertyAlbumArtist, - appName ? appName : @"AppName", MPMediaItemPropertyTitle, - [NSNumber numberWithFloat:isPlaying ? 1.0f : 0.0], MPNowPlayingInfoPropertyPlaybackRate, nil]; + self.currentSong ? self.currentSong : @"Playing an audio file", MPMediaItemPropertyAlbumTitle, + @"", MPMediaItemPropertyAlbumArtist, + appName ? appName : @"AppName", MPMediaItemPropertyTitle, + [NSNumber numberWithDouble: self.audioPlayer.progress], MPNowPlayingInfoPropertyElapsedPlaybackTime, + [NSNumber numberWithDouble: self.audioPlayer.duration], MPMediaItemPropertyPlaybackDuration, + [NSNumber numberWithFloat:isPlaying ? 1.0f : 0.0], MPNowPlayingInfoPropertyPlaybackRate, nil]; [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo; } else { [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nil;