diff --git a/android/build.gradle b/android/build.gradle
index 2c57aa3..6958f59 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -5,7 +5,7 @@ buildscript {
// version numbers
ext.kotlin_version = '1.3.50'
- ext.exo_player_version = '2.11.2'
+ ext.exo_player_version = '2.12.1'
repositories {
google()
@@ -51,4 +51,6 @@ dependencies {
implementation "com.google.android.exoplayer:exoplayer:$exo_player_version"
implementation "com.google.android.exoplayer:extension-mediasession:$exo_player_version"
+ compileOnly files('tmplibs/flutter.jar')
+
}
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 01a286e..f3d0e8a 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,6 @@
+#Thu Oct 15 12:19:13 EET 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 53576c8..dc741f1 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -6,8 +6,9 @@
-
-
+
+
diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/FlutterRadioPlayerPlugin.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/FlutterRadioPlayerPlugin.kt
index bdd083e..2bb3bbb 100644
--- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/FlutterRadioPlayerPlugin.kt
+++ b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/FlutterRadioPlayerPlugin.kt
@@ -1,10 +1,19 @@
package me.sithiramunasinghe.flutter.flutter_radio_player
+import android.app.Activity
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.*
+import android.content.res.AssetFileDescriptor
+import android.content.res.AssetManager
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
import android.os.IBinder
import androidx.annotation.NonNull
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.embedding.engine.plugins.activity.ActivityAware
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
@@ -17,11 +26,15 @@ import io.flutter.plugin.common.PluginRegistry.Registrar
import me.sithiramunasinghe.flutter.flutter_radio_player.core.PlayerItem
import me.sithiramunasinghe.flutter.flutter_radio_player.core.StreamingCore
import me.sithiramunasinghe.flutter.flutter_radio_player.core.enums.PlayerMethods
+import java.util.*
import java.util.logging.Logger
+import kotlin.concurrent.schedule
+
/** FlutterRadioPlayerPlugin */
-public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
+public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private var logger = Logger.getLogger(FlutterRadioPlayerPlugin::javaClass.name)
+ public var activity: Activity? = null
private lateinit var methodChannel: MethodChannel
@@ -29,6 +42,7 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
private var mEventMetaDataSink: EventSink? = null
companion object {
+
@JvmStatic
fun registerWith(registrar: Registrar) {
val instance = FlutterRadioPlayerPlugin()
@@ -45,9 +59,11 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
var isBound = false
lateinit var applicationContext: Context
lateinit var coreService: StreamingCore
+ var bitmap: Bitmap? = null
lateinit var serviceIntent: Intent
}
+
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
buildEngine(flutterPluginBinding.applicationContext, flutterPluginBinding.binaryMessenger)
}
@@ -69,6 +85,11 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
play()
result.success(null)
}
+ PlayerMethods.NEW_PLAY.value -> {
+ logger.info("newPlay service invoked")
+ newPlay()
+ result.success(null)
+ }
PlayerMethods.PAUSE.value -> {
logger.info("pause service invoked")
pause()
@@ -79,6 +100,13 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
stop()
result.success(null)
}
+ PlayerMethods.SET_TITLE.value -> {
+ logger.info("setTitle service invoked")
+ val title = call.argument("title")!!
+ val subTitle = call.argument("subtitle")!!
+ coreService.setTitle(title,subTitle)
+ result.success(null)
+ }
PlayerMethods.INIT.value -> {
logger.info("start service invoked")
init(call)
@@ -93,6 +121,12 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
PlayerMethods.SET_URL.value -> {
logger.info("Set url invoked")
val url = call.argument("streamUrl")!!
+ val coverByteArray = call.argument("coverImage")
+ if (coverByteArray != null){
+ bitmap = BitmapFactory.decodeByteArray(coverByteArray,0 ,coverByteArray.size)
+ coreService.iconBitmap = bitmap
+ }
+
val playWhenReady = call.argument("playWhenReady")!!
setUrl(url, playWhenReady)
}
@@ -118,7 +152,6 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
applicationContext = context
serviceIntent = Intent(applicationContext, StreamingCore::class.java)
-
initEventChannelStatus(messenger)
initEventChannelMetaData(messenger)
@@ -179,14 +212,25 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
private fun init(methodCall: MethodCall) {
logger.info("Attempting to initialize service...")
+
+ val coverByteArray = methodCall.argument("coverImage")
+ if (coverByteArray != null){
+ bitmap = BitmapFactory.decodeByteArray(coverByteArray,0 ,coverByteArray.size)
+ }
+
if (!isBound) {
logger.info("Service not bound, binding now....")
serviceIntent = setIntentData(serviceIntent, buildPlayerDetailsMeta(methodCall))
applicationContext.bindService(serviceIntent, serviceConnection, Context.BIND_IMPORTANT)
applicationContext.startService(serviceIntent)
+ } else {
+ Timer("SettingUp", false).schedule(500) {
+ coreService.reEmmitSatus()
+ }
}
}
+
private fun isPlaying(): Boolean {
logger.info("Attempting to get playing status....")
val playingStatus = coreService.isPlaying()
@@ -199,11 +243,17 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
if (isPlaying()) pause() else play()
}
+
private fun play() {
- logger.info("Attempting to play music....")
+ logger.info("Attempting to Play music....")
coreService.play()
}
+ private fun newPlay() {
+ logger.info("Attempting to newPlay music....")
+ coreService.newPlay()
+ }
+
private fun pause() {
logger.info("Attempting to pause music....")
coreService.pause()
@@ -244,14 +294,20 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
override fun onServiceDisconnected(name: ComponentName?) {
isBound = false
// coreService = null
+ logger.info("Service Disconnected...")
}
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
val localBinder = binder as StreamingCore.LocalBinder
coreService = localBinder.service
+ coreService.activity = this@FlutterRadioPlayerPlugin.activity
isBound = true
+// coreService.reEmmitSatus()
logger.info("Service Connection Established...")
logger.info("Service bounded...")
+
+ coreService.iconBitmap = bitmap
+
}
}
@@ -260,6 +316,7 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
*/
private var broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
+
if (intent != null) {
val returnStatus = intent.getStringExtra("status")
logger.info("Received status: $returnStatus")
@@ -281,4 +338,17 @@ public class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
}
}
}
+
+ override fun onDetachedFromActivity() {
+ }
+
+ override fun onReattachedToActivityForConfigChanges(p0: ActivityPluginBinding) {
+ }
+
+ override fun onAttachedToActivity(p0: ActivityPluginBinding) {
+ this.activity = p0.activity
+ }
+
+ override fun onDetachedFromActivityForConfigChanges() {
+ }
}
diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/Constants.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/Constants.kt
index a7eed65..72260df 100644
--- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/Constants.kt
+++ b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/Constants.kt
@@ -4,5 +4,6 @@ const val FLUTTER_RADIO_PLAYER_STOPPED = "flutter_radio_stopped"
const val FLUTTER_RADIO_PLAYER_PLAYING = "flutter_radio_playing"
const val FLUTTER_RADIO_PLAYER_PAUSED = "flutter_radio_paused"
const val FLUTTER_RADIO_PLAYER_LOADING = "flutter_radio_loading"
+//const val FLUTTER_RADIO_PLAYER_READY = "flutter_radio_ready"
const val FLUTTER_RADIO_PLAYER_ERROR = "flutter_radio_error"
\ No newline at end of file
diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/CustomControlDispatcher.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/CustomControlDispatcher.kt
new file mode 100644
index 0000000..77affee
--- /dev/null
+++ b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/CustomControlDispatcher.kt
@@ -0,0 +1,150 @@
+package me.sithiramunasinghe.flutter.flutter_radio_player.core
+
+import com.google.android.exoplayer2.C
+import com.google.android.exoplayer2.ControlDispatcher
+import com.google.android.exoplayer2.Player
+import com.google.android.exoplayer2.Timeline
+
+/*
+* Copyright (C) 2017 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+/** Default [ControlDispatcher]. */
+class CustomControlDispatcher @JvmOverloads constructor(
+ /** Returns the fast forward increment in milliseconds. */
+ @set:Deprecated("""Create a new instance instead and pass the new instance to the UI component. This
+ makes sure the UI gets updated and is in sync with the new values.""") var fastForwardIncrementMs: Long = CustomControlDispatcher.Companion.DEFAULT_FAST_FORWARD_MS.toLong(),
+ /** Returns the rewind increment in milliseconds. */
+ @set:Deprecated("""Create a new instance instead and pass the new instance to the UI component. This
+ makes sure the UI gets updated and is in sync with the new values.""") var rewindIncrementMs: Long = CustomControlDispatcher.Companion.DEFAULT_REWIND_MS.toLong()) : ControlDispatcher {
+ private val window: Timeline.Window
+
+ override fun dispatchSetPlayWhenReady(player: Player, playWhenReady: Boolean): Boolean {
+ if (!playWhenReady)
+ player.playWhenReady = playWhenReady
+ else{
+ player.stop()
+ player.prepare()
+ player.playWhenReady = playWhenReady
+ }
+ return true
+ }
+
+ override fun dispatchSeekTo(player: Player, windowIndex: Int, positionMs: Long): Boolean {
+ player.seekTo(windowIndex, positionMs)
+ return true
+ }
+
+ override fun dispatchPrevious(player: Player): Boolean {
+ val timeline = player.currentTimeline
+ if (timeline.isEmpty || player.isPlayingAd) {
+ return true
+ }
+ val windowIndex = player.currentWindowIndex
+ timeline.getWindow(windowIndex, window)
+ val previousWindowIndex = player.previousWindowIndex
+ if (previousWindowIndex != C.INDEX_UNSET
+ && (player.currentPosition <= CustomControlDispatcher.Companion.MAX_POSITION_FOR_SEEK_TO_PREVIOUS
+ || window.isDynamic && !window.isSeekable)) {
+ player.seekTo(previousWindowIndex, C.TIME_UNSET)
+ } else {
+ player.seekTo(windowIndex, /* positionMs= */0)
+ }
+ return true
+ }
+
+ override fun dispatchNext(player: Player): Boolean {
+ val timeline = player.currentTimeline
+ if (timeline.isEmpty || player.isPlayingAd) {
+ return true
+ }
+ val windowIndex = player.currentWindowIndex
+ val nextWindowIndex = player.nextWindowIndex
+ if (nextWindowIndex != C.INDEX_UNSET) {
+ player.seekTo(nextWindowIndex, C.TIME_UNSET)
+ } else if (timeline.getWindow(windowIndex, window).isLive) {
+ player.seekTo(windowIndex, C.TIME_UNSET)
+ }
+ return true
+ }
+
+ override fun dispatchRewind(player: Player): Boolean {
+ if (isRewindEnabled && player.isCurrentWindowSeekable) {
+ CustomControlDispatcher.Companion.seekToOffset(player, -rewindIncrementMs)
+ }
+ return true
+ }
+
+ override fun dispatchFastForward(player: Player): Boolean {
+ if (isFastForwardEnabled && player.isCurrentWindowSeekable) {
+ CustomControlDispatcher.Companion.seekToOffset(player, fastForwardIncrementMs)
+ }
+ return true
+ }
+
+ override fun dispatchSetRepeatMode(player: Player, @Player.RepeatMode repeatMode: Int): Boolean {
+ player.repeatMode = repeatMode
+ return true
+ }
+
+ override fun dispatchSetShuffleModeEnabled(player: Player, shuffleModeEnabled: Boolean): Boolean {
+ player.shuffleModeEnabled = shuffleModeEnabled
+ return true
+ }
+
+ override fun dispatchStop(player: Player, reset: Boolean): Boolean {
+ player.stop(reset)
+ return true
+ }
+
+ override fun isRewindEnabled(): Boolean {
+ return rewindIncrementMs > 0
+ }
+
+ override fun isFastForwardEnabled(): Boolean {
+ return fastForwardIncrementMs > 0
+ }
+
+ companion object {
+ /** The default fast forward increment, in milliseconds. */
+ const val DEFAULT_FAST_FORWARD_MS = 15000
+
+ /** The default rewind increment, in milliseconds. */
+ const val DEFAULT_REWIND_MS = 5000
+ private const val MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000
+
+ // Internal methods.
+ private fun seekToOffset(player: Player, offsetMs: Long) {
+ var positionMs = player.currentPosition + offsetMs
+ val durationMs = player.duration
+ if (durationMs != C.TIME_UNSET) {
+ positionMs = Math.min(positionMs, durationMs)
+ }
+ positionMs = Math.max(positionMs, 0)
+ player.seekTo(player.currentWindowIndex, positionMs)
+ }
+ }
+ /**
+ * Creates an instance with the given increments.
+ *
+ * @param fastForwardIncrementMs The fast forward increment in milliseconds. A non-positive value
+ * disables the fast forward operation.
+ * @param rewindIncrementMs The rewind increment in milliseconds. A non-positive value disables
+ * the rewind operation.
+ */
+ /** Creates an instance. */
+ init {
+ window = Timeline.Window()
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/CustomLoadControl.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/CustomLoadControl.kt
new file mode 100644
index 0000000..346439a
--- /dev/null
+++ b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/CustomLoadControl.kt
@@ -0,0 +1,432 @@
+package me.sithiramunasinghe.flutter.flutter_radio_player.core
+
+import com.google.android.exoplayer2.C
+import com.google.android.exoplayer2.LoadControl
+import com.google.android.exoplayer2.Renderer
+import com.google.android.exoplayer2.source.TrackGroupArray
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray
+import com.google.android.exoplayer2.upstream.Allocator
+import com.google.android.exoplayer2.upstream.DefaultAllocator
+import com.google.android.exoplayer2.util.Assertions
+import com.google.android.exoplayer2.util.Log
+import com.google.android.exoplayer2.util.Util
+//
+//class CustomLoadControl {
+//}
+
+
+/**
+ * The default [LoadControl] implementation.
+ */
+class CustomLoadControl protected constructor(
+ allocator: DefaultAllocator,
+ minBufferMs: Int,
+ maxBufferMs: Int,
+ bufferForPlaybackMs: Int,
+ bufferForPlaybackAfterRebufferMs: Int,
+ targetBufferBytes: Int,
+ prioritizeTimeOverSizeThresholds: Boolean,
+ backBufferDurationMs: Int,
+ retainBackBufferFromKeyframe: Boolean) : LoadControl {
+ /** Builder for [CustomLoadControl]. */
+ class Builder {
+ private var allocator: DefaultAllocator? = null
+ private var minBufferMs: Int
+ private var maxBufferMs: Int
+ private var bufferForPlaybackMs: Int
+ private var bufferForPlaybackAfterRebufferMs: Int
+ private var targetBufferBytes: Int
+ private var prioritizeTimeOverSizeThresholds: Boolean
+ private var backBufferDurationMs: Int
+ private var retainBackBufferFromKeyframe: Boolean
+ private var buildCalled = false
+
+ /**
+ * Sets the [DefaultAllocator] used by the loader.
+ *
+ * @param allocator The [DefaultAllocator].
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If [.build] has already been called.
+ */
+ fun setAllocator(allocator: DefaultAllocator?): Builder {
+ Assertions.checkState(!buildCalled)
+ this.allocator = allocator
+ return this
+ }
+
+ /**
+ * Sets the buffer duration parameters.
+ *
+ * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
+ * buffered at all times, in milliseconds.
+ * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in
+ * milliseconds.
+ * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start
+ * or resume following a user action such as a seek, in milliseconds.
+ * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered
+ * for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be
+ * caused by buffer depletion rather than a user action.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If [.build] has already been called.
+ */
+ fun setBufferDurationsMs(
+ minBufferMs: Int,
+ maxBufferMs: Int,
+ bufferForPlaybackMs: Int,
+ bufferForPlaybackAfterRebufferMs: Int): Builder {
+ Assertions.checkState(!buildCalled)
+ assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0")
+ assertGreaterOrEqual(
+ bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0")
+ assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs")
+ assertGreaterOrEqual(
+ minBufferMs,
+ bufferForPlaybackAfterRebufferMs,
+ "minBufferMs",
+ "bufferForPlaybackAfterRebufferMs")
+ assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs")
+ this.minBufferMs = minBufferMs
+ this.maxBufferMs = maxBufferMs
+ this.bufferForPlaybackMs = bufferForPlaybackMs
+ this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs
+ return this
+ }
+
+ /**
+ * Sets the target buffer size in bytes. If set to [C.LENGTH_UNSET], the target buffer
+ * size will be calculated based on the selected tracks.
+ *
+ * @param targetBufferBytes The target buffer size in bytes.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If [.build] has already been called.
+ */
+ fun setTargetBufferBytes(targetBufferBytes: Int): Builder {
+ Assertions.checkState(!buildCalled)
+ this.targetBufferBytes = targetBufferBytes
+ return this
+ }
+
+ /**
+ * Sets whether the load control prioritizes buffer time constraints over buffer size
+ * constraints.
+ *
+ * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time
+ * constraints over buffer size constraints.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If [.build] has already been called.
+ */
+ fun setPrioritizeTimeOverSizeThresholds(prioritizeTimeOverSizeThresholds: Boolean): Builder {
+ Assertions.checkState(!buildCalled)
+ this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds
+ return this
+ }
+
+ /**
+ * Sets the back buffer duration, and whether the back buffer is retained from the previous
+ * keyframe.
+ *
+ * @param backBufferDurationMs The back buffer duration in milliseconds.
+ * @param retainBackBufferFromKeyframe Whether the back buffer is retained from the previous
+ * keyframe.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If [.build] has already been called.
+ */
+ fun setBackBuffer(backBufferDurationMs: Int, retainBackBufferFromKeyframe: Boolean): Builder {
+ Assertions.checkState(!buildCalled)
+ assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0")
+ this.backBufferDurationMs = backBufferDurationMs
+ this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe
+ return this
+ }
+
+ @Deprecated("use {@link #build} instead. ")
+ fun createCustomLoadControl(): CustomLoadControl {
+ return build()
+ }
+
+ /** Creates a [CustomLoadControl]. */
+ fun build(): CustomLoadControl {
+ Assertions.checkState(!buildCalled)
+ buildCalled = true
+ if (allocator == null) {
+ allocator = DefaultAllocator( /* trimOnReset= */true, C.DEFAULT_BUFFER_SEGMENT_SIZE)
+ }
+ return CustomLoadControl(
+ allocator!!,
+ minBufferMs,
+ maxBufferMs,
+ bufferForPlaybackMs,
+ bufferForPlaybackAfterRebufferMs,
+ targetBufferBytes,
+ prioritizeTimeOverSizeThresholds,
+ backBufferDurationMs,
+ retainBackBufferFromKeyframe)
+ }
+
+ /** Constructs a new instance. */
+ init {
+ minBufferMs = DEFAULT_MIN_BUFFER_MS
+ maxBufferMs = DEFAULT_MAX_BUFFER_MS
+ bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS
+ bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
+ targetBufferBytes = DEFAULT_TARGET_BUFFER_BYTES
+ prioritizeTimeOverSizeThresholds = DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS
+ backBufferDurationMs = DEFAULT_BACK_BUFFER_DURATION_MS
+ retainBackBufferFromKeyframe = DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME
+ }
+ }
+
+ private val allocator: DefaultAllocator
+ private val minBufferUs: Long
+ private val maxBufferUs: Long
+ private val bufferForPlaybackUs: Long
+ private val bufferForPlaybackAfterRebufferUs: Long
+ private val targetBufferBytesOverwrite: Int
+ private val prioritizeTimeOverSizeThresholds: Boolean
+ private val backBufferDurationUs: Long
+ private val retainBackBufferFromKeyframe: Boolean
+ private var targetBufferBytes: Int
+ private var isBuffering = false
+
+ /** Constructs a new instance, using the `DEFAULT_*` constants defined in this class. */
+ constructor() : this(DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)) {}
+
+ @Deprecated("Use {@link Builder} instead. ")
+ constructor(allocator: DefaultAllocator) : this(
+ allocator,
+ DEFAULT_MIN_BUFFER_MS,
+ DEFAULT_MAX_BUFFER_MS,
+ DEFAULT_BUFFER_FOR_PLAYBACK_MS,
+ DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
+ DEFAULT_TARGET_BUFFER_BYTES,
+ DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS,
+ DEFAULT_BACK_BUFFER_DURATION_MS,
+ DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME) {
+ }
+
+ @Deprecated("Use {@link Builder} instead. ")
+ constructor(
+ allocator: DefaultAllocator,
+ minBufferMs: Int,
+ maxBufferMs: Int,
+ bufferForPlaybackMs: Int,
+ bufferForPlaybackAfterRebufferMs: Int,
+ targetBufferBytes: Int,
+ prioritizeTimeOverSizeThresholds: Boolean) : this(
+ allocator,
+ minBufferMs,
+ maxBufferMs,
+ bufferForPlaybackMs,
+ bufferForPlaybackAfterRebufferMs,
+ targetBufferBytes,
+ prioritizeTimeOverSizeThresholds,
+ DEFAULT_BACK_BUFFER_DURATION_MS,
+ DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME) {
+ }
+
+ override fun onPrepared() {
+ reset(false)
+ }
+
+ override fun onTracksSelected(renderers: Array, trackGroups: TrackGroupArray,
+ trackSelections: TrackSelectionArray) {
+ targetBufferBytes = if (targetBufferBytesOverwrite == C.LENGTH_UNSET) calculateTargetBufferBytes(renderers, trackSelections) else targetBufferBytesOverwrite
+ allocator.setTargetBufferSize(targetBufferBytes)
+ }
+
+ override fun onStopped() {
+ reset(true)
+ }
+
+ override fun onReleased() {
+ reset(true)
+ }
+
+ override fun getAllocator(): Allocator {
+ return allocator
+ }
+
+ override fun getBackBufferDurationUs(): Long {
+ return backBufferDurationUs
+ }
+
+ override fun retainBackBufferFromKeyframe(): Boolean {
+ return retainBackBufferFromKeyframe
+ }
+
+ override fun shouldContinueLoading(
+ playbackPositionUs: Long, bufferedDurationUs: Long, playbackSpeed: Float): Boolean {
+
+ val targetBufferSizeReached = allocator.totalBytesAllocated >= targetBufferBytes
+ var minBufferUs = minBufferUs
+ if (playbackSpeed > 1) {
+ // The playback speed is faster than real time, so scale up the minimum required media
+ // duration to keep enough media buffered for a playout duration of minBufferUs.
+ val mediaDurationMinBufferUs = Util.getMediaDurationForPlayoutDuration(minBufferUs, playbackSpeed)
+ minBufferUs = Math.min(mediaDurationMinBufferUs, maxBufferUs)
+ }
+
+ // Prevent playback from getting stuck if minBufferUs is too small.
+ minBufferUs = Math.max(minBufferUs, 500000)
+ if (bufferedDurationUs < minBufferUs) {
+ isBuffering = prioritizeTimeOverSizeThresholds || !targetBufferSizeReached
+ if (!isBuffering && bufferedDurationUs < 500000) {
+ Log.w(
+ "CustomLoadControl",
+ "Target buffer size reached with less than 500ms of buffered media data.")
+ }
+ } else if (bufferedDurationUs >= maxBufferUs || targetBufferSizeReached) {
+ isBuffering = false
+ } // Else don't change the buffering state
+ Log.d("CustomLoadControl", "bufferedDurationUs:$bufferedDurationUs," +
+ " playbackPositionUs:$playbackPositionUs," +
+ " maxBufferUs:$maxBufferUs," +
+ " isBuffering:$isBuffering," +
+ " targetBufferSizeReached:$targetBufferSizeReached")
+ return isBuffering
+ }
+
+ override fun shouldStartPlayback(
+ bufferedDurationUs: Long, playbackSpeed: Float, rebuffering: Boolean): Boolean {
+ var bufferedDurationUs = bufferedDurationUs
+ bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed)
+ val minBufferDurationUs = if (rebuffering) bufferForPlaybackAfterRebufferUs else bufferForPlaybackUs
+ val shouldStart = minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs || (!prioritizeTimeOverSizeThresholds
+ && allocator.totalBytesAllocated >= targetBufferBytes)
+ Log.d("CustomLoadControl:shouldStartPlayback", "bufferedDurationUs:$bufferedDurationUs, shouldStart:$shouldStart")
+
+ return shouldStart
+ }
+
+ /**
+ * Calculate target buffer size in bytes based on the selected tracks. The player will try not to
+ * exceed this target buffer. Only used when `targetBufferBytes` is [C.LENGTH_UNSET].
+ *
+ * @param renderers The renderers for which the track were selected.
+ * @param trackSelectionArray The selected tracks.
+ * @return The target buffer size in bytes.
+ */
+ protected fun calculateTargetBufferBytes(
+ renderers: Array, trackSelectionArray: TrackSelectionArray): Int {
+ var targetBufferSize = 0
+ for (i in renderers.indices) {
+ if (trackSelectionArray[i] != null) {
+ targetBufferSize += getDefaultBufferSize(renderers[i].trackType)
+ }
+ }
+ Log.d("calculateTargetBufferBytes","${Math.max(DEFAULT_MIN_BUFFER_SIZE, targetBufferSize)}")
+ return Math.max(DEFAULT_MIN_BUFFER_SIZE, targetBufferSize)
+ }
+
+ private fun reset(resetAllocator: Boolean) {
+ targetBufferBytes = if (targetBufferBytesOverwrite == C.LENGTH_UNSET) DEFAULT_MIN_BUFFER_SIZE else targetBufferBytesOverwrite
+ isBuffering = false
+ if (resetAllocator) {
+ allocator.reset()
+ }
+ }
+
+ companion object {
+ /**
+ * The default minimum duration of media that the player will attempt to ensure is buffered at all
+ * times, in milliseconds.
+ */
+ const val DEFAULT_MIN_BUFFER_MS = 50000
+
+ /**
+ * The default maximum duration of media that the player will attempt to buffer, in milliseconds.
+ */
+ const val DEFAULT_MAX_BUFFER_MS = 50000
+
+ /**
+ * The default duration of media that must be buffered for playback to start or resume following a
+ * user action such as a seek, in milliseconds.
+ */
+ const val DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500
+
+ /**
+ * The default duration of media that must be buffered for playback to resume after a rebuffer, in
+ * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action.
+ */
+ const val DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000
+
+ /**
+ * The default target buffer size in bytes. The value ([C.LENGTH_UNSET]) means that the load
+ * control will calculate the target buffer size based on the selected tracks.
+ */
+ const val DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET
+
+ /** The default prioritization of buffer time constraints over size constraints. */
+ const val DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = false
+
+ /** The default back buffer duration in milliseconds. */
+ const val DEFAULT_BACK_BUFFER_DURATION_MS = 0
+
+ /** The default for whether the back buffer is retained from the previous keyframe. */
+ const val DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME = false
+
+ /** A default size in bytes for a video buffer. */
+ const val DEFAULT_VIDEO_BUFFER_SIZE = 2000 * C.DEFAULT_BUFFER_SEGMENT_SIZE
+
+ /** A default size in bytes for an audio buffer. */
+ const val DEFAULT_AUDIO_BUFFER_SIZE = 200 * C.DEFAULT_BUFFER_SEGMENT_SIZE
+
+ /** A default size in bytes for a text buffer. */
+ const val DEFAULT_TEXT_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE
+
+ /** A default size in bytes for a metadata buffer. */
+ const val DEFAULT_METADATA_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE
+
+ /** A default size in bytes for a camera motion buffer. */
+ const val DEFAULT_CAMERA_MOTION_BUFFER_SIZE = 2 * C.DEFAULT_BUFFER_SEGMENT_SIZE
+
+ /** A default size in bytes for a muxed buffer (e.g. containing video, audio and text). */
+ const val DEFAULT_MUXED_BUFFER_SIZE = DEFAULT_VIDEO_BUFFER_SIZE + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE
+
+ /**
+ * The buffer size in bytes that will be used as a minimum target buffer in all cases. This is
+ * also the default target buffer before tracks are selected.
+ */
+ const val DEFAULT_MIN_BUFFER_SIZE = 200 * C.DEFAULT_BUFFER_SEGMENT_SIZE
+ private fun getDefaultBufferSize(trackType: Int): Int {
+ return when (trackType) {
+ C.TRACK_TYPE_DEFAULT -> DEFAULT_MUXED_BUFFER_SIZE
+ C.TRACK_TYPE_AUDIO -> DEFAULT_AUDIO_BUFFER_SIZE
+ C.TRACK_TYPE_VIDEO -> DEFAULT_VIDEO_BUFFER_SIZE
+ C.TRACK_TYPE_TEXT -> DEFAULT_TEXT_BUFFER_SIZE
+ C.TRACK_TYPE_METADATA -> DEFAULT_METADATA_BUFFER_SIZE
+ C.TRACK_TYPE_CAMERA_MOTION -> DEFAULT_CAMERA_MOTION_BUFFER_SIZE
+ C.TRACK_TYPE_NONE -> 0
+ else -> throw IllegalArgumentException()
+ }
+ }
+
+ private fun assertGreaterOrEqual(value1: Int, value2: Int, name1: String, name2: String) {
+ Assertions.checkArgument(value1 >= value2, "$name1 cannot be less than $name2")
+ }
+ }
+
+ init {
+ assertGreaterOrEqual(bufferForPlaybackMs, 0, "bufferForPlaybackMs", "0")
+ assertGreaterOrEqual(
+ bufferForPlaybackAfterRebufferMs, 0, "bufferForPlaybackAfterRebufferMs", "0")
+ assertGreaterOrEqual(minBufferMs, bufferForPlaybackMs, "minBufferMs", "bufferForPlaybackMs")
+ assertGreaterOrEqual(
+ minBufferMs,
+ bufferForPlaybackAfterRebufferMs,
+ "minBufferMs",
+ "bufferForPlaybackAfterRebufferMs")
+ assertGreaterOrEqual(maxBufferMs, minBufferMs, "maxBufferMs", "minBufferMs")
+ assertGreaterOrEqual(backBufferDurationMs, 0, "backBufferDurationMs", "0")
+ this.allocator = allocator
+ minBufferUs = C.msToUs(minBufferMs.toLong())
+ maxBufferUs = C.msToUs(maxBufferMs.toLong())
+ bufferForPlaybackUs = C.msToUs(bufferForPlaybackMs.toLong())
+ bufferForPlaybackAfterRebufferUs = C.msToUs(bufferForPlaybackAfterRebufferMs.toLong())
+ targetBufferBytesOverwrite = targetBufferBytes
+ this.targetBufferBytes = if (targetBufferBytesOverwrite != C.LENGTH_UNSET) targetBufferBytesOverwrite else DEFAULT_MIN_BUFFER_SIZE
+ this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds
+ backBufferDurationUs = C.msToUs(backBufferDurationMs.toLong())
+ this.retainBackBufferFromKeyframe = retainBackBufferFromKeyframe
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/StreamingCore.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/StreamingCore.kt
index 3bb5809..32cb351 100644
--- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/StreamingCore.kt
+++ b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/StreamingCore.kt
@@ -1,39 +1,43 @@
package me.sithiramunasinghe.flutter.flutter_radio_player.core
-import android.app.Notification
-import android.app.PendingIntent
-import android.app.Service
+import android.app.*
+import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
+import android.media.AudioFocusRequest
import android.media.AudioManager
+import android.media.audiofx.AudioEffect
import android.media.session.MediaSession
import android.net.Uri
import android.os.Binder
import android.os.Build
+import android.os.Handler
import android.os.IBinder
import android.support.v4.media.session.MediaSessionCompat
import androidx.annotation.Nullable
import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import com.google.android.exoplayer2.C
-import com.google.android.exoplayer2.ExoPlaybackException
-import com.google.android.exoplayer2.Player
-import com.google.android.exoplayer2.SimpleExoPlayer
+import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager
+import com.google.android.exoplayer2.upstream.DefaultAllocator
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util
import me.sithiramunasinghe.flutter.flutter_radio_player.FlutterRadioPlayerPlugin.Companion.broadcastActionName
import me.sithiramunasinghe.flutter.flutter_radio_player.FlutterRadioPlayerPlugin.Companion.broadcastChangedMetaDataName
import me.sithiramunasinghe.flutter.flutter_radio_player.R
import me.sithiramunasinghe.flutter.flutter_radio_player.core.enums.PlaybackStatus
+import java.util.concurrent.TimeUnit
import java.util.logging.Logger
class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
private var logger = Logger.getLogger(StreamingCore::javaClass.name)
+ var activity: Activity? = null
+ var iconBitmap: Bitmap? = null
+
private var isBound = false
private val iBinder = LocalBinder()
@@ -46,12 +50,42 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
private val broadcastIntent = Intent(broadcastActionName)
private val broadcastMetaDataIntent = Intent(broadcastChangedMetaDataName)
+
// class instances
+ private val handler = Handler();
+
+ private var audioManager: AudioManager? = null
+ private var focusRequest: AudioFocusRequest? = null
private var player: SimpleExoPlayer? = null
private var mediaSessionConnector: MediaSessionConnector? = null
private var mediaSession: MediaSession? = null
private var playerNotificationManager: PlayerNotificationManager? = null
+ var notificationTitle = ""
+ var notificationSubTitle = ""
+
+ val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
+ when (focusChange) {
+ AudioManager.AUDIOFOCUS_LOSS -> {
+ logger.info("AUDIOFOCUS_LOSS")
+
+ pause()
+ handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
+ logger.info("AUDIOFOCUS_LOSS_TRANSIENT")
+ pause()
+ }
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
+ setVolume(0.1)
+ }
+ AudioManager.AUDIOFOCUS_GAIN -> {
+ setVolume(1.0)
+ newPlay()
+ }
+ }
+ }
+
// session keys
private val playbackNotificationId = 1025
private val mediaSessionId = "streaming_audio_player_media_session"
@@ -69,7 +103,28 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
fun play() {
logger.info("playing audio $player ...")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ audioManager!!.requestAudioFocus(focusRequest!!)
+ } else {
+ audioManager!!.requestAudioFocus(afChangeListener, AudioEffect.CONTENT_TYPE_MUSIC, 0);
+ }
+ player?.playWhenReady = true
+ wasPlaying = false
+
+ }
+
+ fun newPlay() {
+ logger.info("new Play audio $player ...")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ audioManager!!.requestAudioFocus(focusRequest!!)
+ } else {
+ audioManager!!.requestAudioFocus(afChangeListener, AudioEffect.CONTENT_TYPE_MUSIC, 0);
+ }
+ player?.stop()
+ player?.prepare()
player?.playWhenReady = true
+ wasPlaying = false
+
}
fun pause() {
@@ -77,12 +132,44 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
player?.playWhenReady = false
}
+
+ fun reEmmitSatus() {
+ logger.info("reEmmtSatus ...")
+ if (this::playbackStatus.isInitialized) {
+ playbackStatus = when (playbackStatus) {
+ PlaybackStatus.PAUSED -> {
+ pushEvent(FLUTTER_RADIO_PLAYER_PAUSED)
+ PlaybackStatus.PAUSED
+ }
+ PlaybackStatus.PLAYING -> {
+ pushEvent(FLUTTER_RADIO_PLAYER_PLAYING)
+ PlaybackStatus.PLAYING
+ }
+ PlaybackStatus.LOADING -> {
+ pushEvent(FLUTTER_RADIO_PLAYER_LOADING)
+ PlaybackStatus.LOADING
+
+ }
+ PlaybackStatus.STOPPED -> {
+ pushEvent(FLUTTER_RADIO_PLAYER_STOPPED)
+ PlaybackStatus.STOPPED
+ }
+ PlaybackStatus.ERROR -> {
+ pushEvent(FLUTTER_RADIO_PLAYER_ERROR)
+ PlaybackStatus.ERROR
+ }
+ }
+ }
+ }
+
fun isPlaying(): Boolean {
val isPlaying = this.playbackStatus == PlaybackStatus.PLAYING
logger?.info("is playing status: $isPlaying")
return isPlaying
}
+ var wasPlaying: Boolean = false
+
fun stop() {
logger.info("stopping audio $player ...")
player?.stop()
@@ -90,6 +177,14 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
isBound = false
}
+ fun setTitle(title: String, subTitle: String) {
+ logger.info("settingTitle $title, $player ...")
+ this.notificationTitle = title
+ this.notificationSubTitle = subTitle
+ logger.info("calling playerNotificationManager.invalidate()...")
+ playerNotificationManager?.invalidate()
+ }
+
fun setVolume(volume: Double) {
logger.info("Changing volume to : $volume")
player?.volume = volume.toFloat()
@@ -98,10 +193,16 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
fun setUrl(streamUrl: String, playWhenReady: Boolean) {
logger.info("ReadyPlay status: $playWhenReady")
logger.info("Set stream URL: $streamUrl")
- player?.prepare(buildMediaSource(dataSourceFactory, streamUrl))
+ player?.setMediaSource(buildMediaSource(dataSourceFactory, streamUrl))
+ player?.prepare()
player?.playWhenReady = playWhenReady
}
+ private var delayedStopRunnable = Runnable {
+// stop()
+ }
+
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
logger.info("Firing up service. (onStartCommand)...")
@@ -111,21 +212,57 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
logger.info("LocalBroadCastManager Received...")
// get details
- val appName = intent!!.getStringExtra("appName")
- val subTitle = intent.getStringExtra("subTitle")
+ notificationTitle = intent!!.getStringExtra("appName")
+ notificationSubTitle = intent.getStringExtra("subTitle")
val streamUrl = intent.getStringExtra("streamUrl")
val playWhenReady = intent.getStringExtra("playWhenReady") == "true"
+ player = SimpleExoPlayer
+ .Builder(context)
+ .setLoadControl(CustomLoadControl
+ .Builder()
+ .setPrioritizeTimeOverSizeThresholds(true)
+ .setBufferDurationsMs(10000,
+ 10000,
+ 10000,
+ 10000)
+ .build()
+ ).build()
+
+
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
+ setAudioAttributes(android.media.AudioAttributes.Builder().run {
+ setUsage(android.media.AudioAttributes.USAGE_MEDIA)
+ setContentType(android.media.AudioAttributes.CONTENT_TYPE_MUSIC)
+ build()
+ })
+ setAcceptsDelayedFocusGain(true)
+ setOnAudioFocusChangeListener(this@StreamingCore, handler)
+ build()
+ }
+ }
+
+ audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
- player = SimpleExoPlayer.Builder(context).build()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ audioManager!!.requestAudioFocus(focusRequest)
+ } else {
+ audioManager!!.requestAudioFocus(afChangeListener, AudioEffect.CONTENT_TYPE_MUSIC, 0);
+ }
- dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, appName))
+ dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, notificationTitle))
val audioSource = buildMediaSource(dataSourceFactory, streamUrl)
val playerEvents = object : Player.EventListener {
- override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
+ override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
playbackStatus = when (playbackState) {
+ Player.STATE_ENDED -> {
+ pushEvent(FLUTTER_RADIO_PLAYER_STOPPED)
+ PlaybackStatus.STOPPED
+ }
Player.STATE_BUFFERING -> {
pushEvent(FLUTTER_RADIO_PLAYER_LOADING)
PlaybackStatus.LOADING
@@ -137,10 +274,21 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
Player.STATE_READY -> {
setPlayWhenReady(playWhenReady)
}
- else -> setPlayWhenReady(playWhenReady)
- }
+ else -> if (this@StreamingCore::playbackStatus.isInitialized) this@StreamingCore.playbackStatus else PlaybackStatus.STOPPED
+ }
+ if (playbackStatus == PlaybackStatus.PLAYING) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ this@StreamingCore.audioManager!!.requestAudioFocus(this@StreamingCore.focusRequest!!)
+ } else {
+ this@StreamingCore.audioManager!!.requestAudioFocus(this@StreamingCore.afChangeListener, AudioEffect.CONTENT_TYPE_MUSIC, 0);
+ }
+ } else {
+ logger.info("Remove player as a foreground notification...")
+ stopForeground(false)
+ }
logger.info("onPlayerStateChanged: $playbackStatus")
+
}
override fun onPlayerError(error: ExoPlaybackException) {
@@ -154,7 +302,8 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
player?.let {
it.addListener(playerEvents)
it.playWhenReady = playWhenReady
- it.prepare(audioSource)
+ it.setMediaSource(audioSource)
+ it.prepare()
}
// register our meta data listener
@@ -170,24 +319,29 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
R.string.channel_description,
playbackNotificationId,
object : PlayerNotificationManager.MediaDescriptionAdapter {
+
override fun getCurrentContentTitle(player: Player): String {
- return appName
+ logger.info("updating title = $notificationTitle")
+ return notificationTitle
}
@Nullable
override fun createCurrentContentIntent(player: Player): PendingIntent {
- return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ val intent = Intent(this@StreamingCore, activity!!.javaClass)
+ val contentPendingIntent = PendingIntent.getActivity(this@StreamingCore, 0, intent, 0);
+ return contentPendingIntent;
}
@Nullable
override fun getCurrentContentText(player: Player): String? {
- return subTitle
+ return null//notificationSubTitle
}
@Nullable
override fun getCurrentLargeIcon(player: Player, callback: PlayerNotificationManager.BitmapCallback): Bitmap? {
- return null // OS will use the application icon.
+ return this@StreamingCore.iconBitmap; // OS will use the application icon.
}
+
},
object : PlayerNotificationManager.NotificationListener {
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
@@ -196,12 +350,15 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
}
override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {
- logger.info("Attaching player as a foreground notification...")
+ logger.info("Attaching player as a foreground notification...ongoing:$ongoing")
startForeground(notificationId, notification)
+ if (!ongoing) {
+ stopForeground(false)
+ }
}
}
)
-
+ this.playerNotificationManager = playerNotificationManager
logger.info("Building Media Session and Player Notification.")
val mediaSession = MediaSessionCompat(context, mediaSessionId)
@@ -210,16 +367,22 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector?.setPlayer(player)
+ val dispatcher = CustomControlDispatcher()
+ dispatcher.fastForwardIncrementMs = 0
+ dispatcher.rewindIncrementMs = 0
+
+ playerNotificationManager.setControlDispatcher(dispatcher)
playerNotificationManager.setUseStopAction(true)
playerNotificationManager.setFastForwardIncrementMs(0)
playerNotificationManager.setRewindIncrementMs(0)
playerNotificationManager.setUsePlayPauseActions(true)
playerNotificationManager.setUseNavigationActions(false)
+ playerNotificationManager.setDefaults(Notification.DEFAULT_ALL)
playerNotificationManager.setUseNavigationActionsInCompactView(false)
-
playerNotificationManager.setPlayer(player)
playerNotificationManager.setMediaSessionToken(mediaSession.sessionToken)
+// playerNotificationManager.
playbackStatus = PlaybackStatus.PLAYING
return START_STICKY
@@ -247,16 +410,23 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
AudioManager.AUDIOFOCUS_GAIN -> {
player?.volume = 0.8f
- play()
+ if (wasPlaying) {
+ newPlay()
+ }
}
AudioManager.AUDIOFOCUS_LOSS -> {
- stop()
+ logger.info("AudioManager.AUDIOFOCUS_LOSS")
+ if (isPlaying()) {
+ pause()
+ }
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
if (isPlaying()) {
- stop()
+ logger.info("AudioManager.AUDIOFOCUS_LOSS_TRANSIENT")
+ pause()
+ wasPlaying = true
}
}
@@ -273,7 +443,7 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
*/
private fun pushEvent(eventName: String) {
logger.info("Pushing Event: $eventName")
- localBroadcastManager.sendBroadcast(broadcastIntent.putExtra("status", eventName))
+ localBroadcastManager.sendBroadcast(Intent(broadcastActionName).putExtra("status", eventName))
}
/**
@@ -284,8 +454,11 @@ class StreamingCore : Service(), AudioManager.OnAudioFocusChangeListener {
val uri = Uri.parse(streamUrl)
return when (val type = Util.inferContentType(uri)) {
- C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
- C.TYPE_OTHER -> ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
+// C.TYPE_DASH -> DashMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri))
+ C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri))
+ C.TYPE_OTHER -> ProgressiveMediaSource.Factory(dataSourceFactory)
+// .setContinueLoadingCheckIntervalBytes(1024*32)
+ .createMediaSource(MediaItem.fromUri(uri))
else -> {
throw IllegalStateException("Unsupported type: $type")
}
diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/enums/PlayerMethods.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/enums/PlayerMethods.kt
index 3974767..e0ef509 100644
--- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/enums/PlayerMethods.kt
+++ b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/enums/PlayerMethods.kt
@@ -2,10 +2,15 @@ package me.sithiramunasinghe.flutter.flutter_radio_player.core.enums
enum class PlayerMethods(val value: String) {
INIT("initService"),
+// INIT("setC"),
+
PLAY_PAUSE("playOrPause"),
PLAY("play"),
+ NEW_PLAY("newPlay"),
+
PAUSE("pause"),
STOP("stop"),
+ SET_TITLE("setTitle"),
SET_URL("setUrl"),
IS_PLAYING("isPlaying"),
SET_VOLUME("setVolume")
diff --git a/android/tmplibs/flutter.jar b/android/tmplibs/flutter.jar
new file mode 100644
index 0000000..21a1d8d
Binary files /dev/null and b/android/tmplibs/flutter.jar differ
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 29b044d..8f83641 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -19,4 +19,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83
-COCOAPODS: 1.8.4
+COCOAPODS: 1.9.3
diff --git a/example/lib/main.dart b/example/lib/main.dart
index d507ac7..9948f3d 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -118,7 +118,7 @@ class _MyAppState extends State {
return Text(snapshot.data);
}),
RaisedButton(child: Text("Change URL"), onPressed: () async {
- _flutterRadioPlayer.setUrl("http://209.133.216.3:7018/;stream.mp3", "false");
+ _flutterRadioPlayer.setUrl("http://209.133.216.3:7018/;stream.mp3", "false",);
})
],
),
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 2937257..2ef5255 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -68,7 +68,7 @@ packages:
path: ".."
relative: true
source: path
- version: "1.0.5"
+ version: "1.0.7"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -158,3 +158,4 @@ packages:
version: "2.0.8"
sdks:
dart: ">=2.9.0-14.0.dev <3.0.0"
+ flutter: ">=1.12.0 <2.0.0"
diff --git a/ios/Classes/SwiftFlutterRadioPlayerPlugin.swift b/ios/Classes/SwiftFlutterRadioPlayerPlugin.swift
index 00caf71..1606e0f 100644
--- a/ios/Classes/SwiftFlutterRadioPlayerPlugin.swift
+++ b/ios/Classes/SwiftFlutterRadioPlayerPlugin.swift
@@ -31,10 +31,14 @@ public class SwiftFlutterRadioPlayerPlugin: NSObject, FlutterPlugin {
let subTitle = args["subTitle"] as? String,
let playWhenReady = args["playWhenReady"] as? String
{
+ if !streamingCore.isFirstTime{
+ NotificationCenter.default.removeObserver(self, name: Notifications.playbackNotification, object: nil)
+ }
+ NotificationCenter.default.addObserver(self, selector: #selector(onRecieve(_:)), name: Notifications.playbackNotification, object: nil)
+
streamingCore.initService(streamURL: streamURL, serviceName: appName, secondTitle: subTitle, playWhenReady: playWhenReady)
- NotificationCenter.default.addObserver(self, selector: #selector(onRecieve(_:)), name: Notifications.playbackNotification, object: nil)
- result(nil)
+ result(false)
}
break
case "playOrPause":
@@ -52,6 +56,25 @@ public class SwiftFlutterRadioPlayerPlugin: NSObject, FlutterPlugin {
}
result(false)
break
+
+ case "newPlay":
+ print("method called to newPlay from service")
+ let status = streamingCore.newPlay()
+ if (status == PlayerStatus.PLAYING) {
+ result(true)
+ }
+ result(false)
+ break
+ case "setTitle":
+ print("method called to set title from service")
+ if let args = call.arguments as? Dictionary,
+ let title = args["title"] as? String,
+ let subTitle = args["subtitle"] as? String
+ {
+ streamingCore.setTitle(title:title,subTitle:subTitle)
+ }
+ result(false)
+ break
case "pause":
print("method called to play from service")
let status = streamingCore.pause()
@@ -93,6 +116,7 @@ public class SwiftFlutterRadioPlayerPlugin: NSObject, FlutterPlugin {
result(nil)
}
}
+
@objc private func onRecieve(_ notification: Notification) {
// unwrapping optional
@@ -105,7 +129,10 @@ public class SwiftFlutterRadioPlayerPlugin: NSObject, FlutterPlugin {
print("Notification received with metada: \(metaDataEvent)")
SwiftFlutterRadioPlayerPlugin.eventSinkMetadata?(metaDataEvent as! String)
}
-
+ }
+
+ deinit {
+ print("DeinitPlugin")
}
}
diff --git a/ios/Classes/core/StreamingCore.swift b/ios/Classes/core/StreamingCore.swift
index a173ec3..84932da 100644
--- a/ios/Classes/core/StreamingCore.swift
+++ b/ios/Classes/core/StreamingCore.swift
@@ -16,28 +16,56 @@ class StreamingCore : NSObject, AVPlayerItemMetadataOutputPushDelegate {
private var playerItemContext = 0
private var commandCenter: MPRemoteCommandCenter?
private var playWhenReady: Bool = false
+ private var wasPlaying: Bool = false
+
+ private var streamUrl:String = ""
+
+ var playerStatus: String = Constants.FLUTTER_RADIO_STOPPED
+
override init() {
print("StreamingCore Initializing...")
}
- func initService(streamURL: String, serviceName: String, secondTitle: String, playWhenReady: String) -> Void {
+ deinit {
+ print("StreamingCore Deinitializing...")
+ self.avPlayer?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.status))
+ self.avPlayer?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.status))
+ self.avPlayer?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackBufferEmpty))
+ }
+
+ fileprivate func removeAvPlayerObserversIfSubscribed() {
+ if !isFirstTime {
+
+ NotificationCenter.default.removeObserver(self, name: .AVPlayerItemNewErrorLogEntry, object: nil)
+ NotificationCenter.default.removeObserver(self, name: .AVPlayerItemFailedToPlayToEndTime, object: nil)
+ NotificationCenter.default.removeObserver(self, name: .AVPlayerItemPlaybackStalled, object: nil)
+
+ self.avPlayer?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.status))
+ self.avPlayer?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.status))
+ self.avPlayer?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackBufferEmpty))
+
+ }
+ }
+
+ func initService(streamURL: String, serviceName: String, secondTitle: String, playWhenReady: String) -> Void {
+ self.streamUrl = streamURL
print("Initialing Service...")
-
+
print("Stream url: " + streamURL)
-
+
let streamURLInstance = URL(string: streamURL)
-
+ removeAvPlayerObserversIfSubscribed()
// Setting up AVPlayer
avPlayerItem = AVPlayerItem(url: streamURLInstance!)
avPlayer = AVPlayer(playerItem: avPlayerItem!)
-
+
//Listener for metadata from streaming
let metadataOutput = AVPlayerItemMetadataOutput(identifiers: nil)
metadataOutput.setDelegate(self, queue: DispatchQueue.main)
avPlayerItem?.add(metadataOutput)
-
+
if playWhenReady == "true" {
print("PlayWhenReady: true")
self.playWhenReady = true
@@ -45,9 +73,85 @@ class StreamingCore : NSObject, AVPlayerItemMetadataOutputPushDelegate {
// initialize player observers
initPlayerObservers()
-
+
// init Remote protocols.
initRemoteTransportControl(appName: serviceName, subTitle: secondTitle);
+ setupNotifications()
+
+ if #available(iOS 10.0, *) {
+ avPlayerItem?.preferredForwardBufferDuration = 10
+ }
+
+ let notificationCenter = NotificationCenter.default
+ if !isFirstTime{
+ notificationCenter.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
+ }
+ notificationCenter.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.didBecomeActiveNotification, object: nil)
+
+ isFirstTime = false
+ }
+
+ @objc
+ func appMovedToForeground() {
+ print("Reemmiting the current state!")
+ pushEvent(eventName: playerStatus)
+ }
+
+ var isFirstTime = true
+
+ func setupNotifications() {
+ // Get the default notification center instance.
+ let nc = NotificationCenter.default
+ if !isFirstTime{
+ nc.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
+ }
+ nc.addObserver(self,
+ selector: #selector(handleInterruption),
+ name: AVAudioSession.interruptionNotification,
+ object: nil)
+
+ }
+
+ @objc func handleInterruption(notification: Notification) {
+ // To be implemented.
+ guard let userInfo = notification.userInfo,
+ let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
+ let type = AVAudioSession.InterruptionType(rawValue: typeValue)
+ else {
+ return
+ }
+
+ switch type {
+ case .began:
+ if #available(iOS 10.3, *) {
+// let suspendedKey = userInfo[AVAudioSessionInterruptionWasSuspendedKey] as? NSNumber ?? 0
+// if suspendedKey == 0 {
+// wasPlaying = false
+// _ = pause()
+// } else {
+ if playerStatus == Constants.FLUTTER_RADIO_PLAYING{
+ wasPlaying = true
+ _ = pause()
+ }
+// }
+ } else {
+ wasPlaying = false
+ _ = pause()
+ // Fallback on earlier versions
+ }
+
+
+ print("an intrruption has begun")
+ break
+ case .ended:
+ if let optionValue = (notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? NSNumber)?.uintValue, AVAudioSession.InterruptionOptions(rawValue: optionValue) == .shouldResume {
+ _ = play()
+ wasPlaying = false
+ }
+
+ break
+ default: ()
+ }
}
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
@@ -55,23 +159,33 @@ class StreamingCore : NSObject, AVPlayerItemMetadataOutputPushDelegate {
{
item.value(forKeyPath: "value")
let song = (item.value(forKeyPath: "value")!)
- pushEvent(typeEvent: "meta_data",eventName: song as! String)
+ pushEvent(typeEvent: "meta_data",eventName: song as! String)
}}
func play() -> PlayerStatus {
print("invoking play method on service")
+ playerStatus = Constants.FLUTTER_RADIO_PLAYING
if(!isPlaying()) {
avPlayer?.play()
pushEvent(eventName: Constants.FLUTTER_RADIO_PLAYING)
}
-
+ return PlayerStatus.PLAYING
+ }
+
+ func newPlay() -> PlayerStatus {
+ print("invoking play method on service")
+ playerStatus = Constants.FLUTTER_RADIO_PLAYING
+ let streamURLInstance = URL(string: streamUrl)
+ playWhenReady = true
+ avPlayer?.replaceCurrentItem(with: AVPlayerItem(url: streamURLInstance!))
return PlayerStatus.PLAYING
}
func pause() -> PlayerStatus {
print("invoking pause method on service")
+ playerStatus = Constants.FLUTTER_RADIO_PAUSED
if (isPlaying()) {
avPlayer?.pause()
pushEvent(eventName: Constants.FLUTTER_RADIO_PAUSED)
@@ -82,11 +196,14 @@ class StreamingCore : NSObject, AVPlayerItemMetadataOutputPushDelegate {
func stop() -> PlayerStatus {
print("invoking stop method on service")
+ playerStatus = Constants.FLUTTER_RADIO_STOPPED
if (isPlaying()) {
pushEvent(eventName: Constants.FLUTTER_RADIO_STOPPED)
+
avPlayer = nil
avPlayerItem = nil
commandCenter = nil
+
}
return PlayerStatus.STOPPED
@@ -103,17 +220,24 @@ class StreamingCore : NSObject, AVPlayerItemMetadataOutputPushDelegate {
print("Setting volume to: \(formattedVolume)")
avPlayer?.volume = formattedVolume
}
+
+ func setTitle(title: String, subTitle:String) -> Void {
+ print("Setting title to: \(title)")
+ let nowPlayingInfo = [MPMediaItemPropertyTitle : title, MPMediaItemPropertyArtist: subTitle]
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
+ }
func setUrl(streamURL: String, playWhenReady: String) -> Void {
+ self.streamUrl = streamURL
let streamURLInstance = URL(string: streamURL)
avPlayer?.replaceCurrentItem(with: AVPlayerItem(url: streamURLInstance!))
if playWhenReady == "true" {
self.playWhenReady = true
- play()
+ _ = play()
} else {
self.playWhenReady = false
- pause()
+ _ = pause()
}
}
@@ -155,9 +279,10 @@ class StreamingCore : NSObject, AVPlayerItemMetadataOutputPushDelegate {
}
// control center play button callback
+// commandCenter?.playCommand.remo
commandCenter?.playCommand.addTarget { (MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus in
print("command center play command...")
- _ = self.play()
+ _ = self.newPlay()
return .success
}
@@ -179,9 +304,10 @@ class StreamingCore : NSObject, AVPlayerItemMetadataOutputPushDelegate {
let audioSession = AVAudioSession.sharedInstance()
if #available(iOS 10.0, *) {
- try audioSession.setCategory(.playback, mode: .default, options: .defaultToSpeaker)
- try audioSession.overrideOutputAudioPort(.speaker)
- try audioSession.setActive(true)
+
+ try? audioSession.setCategory(.playback, mode: .default)
+ try? audioSession.overrideOutputAudioPort(.speaker)
+ try? audioSession.setActive(true)
}
UIApplication.shared.beginReceivingRemoteControlEvents()
@@ -189,29 +315,58 @@ class StreamingCore : NSObject, AVPlayerItemMetadataOutputPushDelegate {
print("Something went wrong ! \(error)")
}
}
-
+
private func initPlayerObservers() {
print("Initializing player observers...")
- // Add observer for AVPlayer.Status and AVPlayerItem.currentItem
- self.avPlayer?.addObserver(self, forKeyPath: #keyPath(AVPlayer.status), options: [.new, .initial], context: nil)
- self.avPlayer?.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.status), options:[.new, .initial], context: nil)
- self.avPlayer?.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackBufferEmpty), options:[.new, .initial], context: nil)
+
+
+ NotificationCenter.default.addObserver(self, selector: #selector(itemNewErrorLogEntry(_:)), name: .AVPlayerItemNewErrorLogEntry, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(itemFailedToPlayToEndTime(_:)), name: .AVPlayerItemFailedToPlayToEndTime, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(itemPlaybackStalled(_:)), name: .AVPlayerItemPlaybackStalled, object: nil)
+
+ self.avPlayer?.addObserver(self, forKeyPath: #keyPath(AVPlayer.status), options: [.new,.initial], context: nil)
+ self.avPlayer?.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.status), options:[.new,.initial], context: nil)
+ self.avPlayer?.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackBufferEmpty), options:[.new,.initial], context: nil)
+ }
+
+
+
+ @objc func itemNewErrorLogEntry(_ notification:Notification){
+ print(notification)
+ }
+
+ @objc func itemFailedToPlayToEndTime(_ notification:Notification){
+ if let _ = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey]{
+ _ = stop()
+ print("Observer: Failed...\(notification.userInfo)")
+ playerStatus = Constants.FLUTTER_RADIO_ERROR
+
+ pushEvent(eventName: Constants.FLUTTER_RADIO_ERROR)
+ }
+ }
+ @objc func itemPlaybackStalled(_ notification:Notification){
+ _ = stop()
+ print("Observer: Stalled...")
+ playerStatus = Constants.FLUTTER_RADIO_ERROR
+
+ pushEvent(eventName: Constants.FLUTTER_RADIO_ERROR)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if object is AVPlayer {
+
switch keyPath {
-
case #keyPath(AVPlayer.currentItem.isPlaybackBufferEmpty):
let _: Bool
if let newStatusNumber = change?[NSKeyValueChangeKey.newKey] as? Bool {
if newStatusNumber {
print("Observer: Stalling...")
+ playerStatus = Constants.FLUTTER_RADIO_LOADING
+
pushEvent(eventName: Constants.FLUTTER_RADIO_LOADING)
}
}
-
case #keyPath(AVPlayer.currentItem.status):
let newStatus: AVPlayerItem.Status
if let newStatusAsNumber = change?[NSKeyValueChangeKey.newKey] as? NSNumber {
@@ -219,30 +374,48 @@ class StreamingCore : NSObject, AVPlayerItemMetadataOutputPushDelegate {
} else {
newStatus = .unknown
}
-
if newStatus == .readyToPlay {
print("Observer: Ready to play...")
if (!isPlaying()) {
if (self.playWhenReady) {
- play()
- }
- pushEvent(eventName: Constants.FLUTTER_RADIO_PAUSED)
+ _ = play()
+ }else{
+ playerStatus = Constants.FLUTTER_RADIO_PAUSED
+ pushEvent(eventName: Constants.FLUTTER_RADIO_PAUSED)}
} else {
+ playerStatus = Constants.FLUTTER_RADIO_PLAYING
+
pushEvent(eventName: Constants.FLUTTER_RADIO_PLAYING)
}
}
if newStatus == .failed {
print("Observer: Failed...")
+ playerStatus = Constants.FLUTTER_RADIO_ERROR
+
pushEvent(eventName: Constants.FLUTTER_RADIO_ERROR)
}
case #keyPath(AVPlayer.status):
- print()
+ var newStatus: AVPlayerItem.Status
+ if let newStatusAsNumber = change?[NSKeyValueChangeKey.newKey] as? NSNumber {
+ newStatus = AVPlayerItem.Status(rawValue: newStatusAsNumber.intValue)!
+ } else {
+ newStatus = .unknown
+ }
+
+ if newStatus == .failed {
+ print("Observer: Failed...")
+
+ playerStatus = Constants.FLUTTER_RADIO_ERROR
+
+ pushEvent(eventName: Constants.FLUTTER_RADIO_ERROR)
+ }
case .none:
print("none...")
case .some(_):
print("some...")
}
+// print(keyPath)
}
}
diff --git a/lib/flutter_radio_player.dart b/lib/flutter_radio_player.dart
index 579f9af..a3fc1fa 100644
--- a/lib/flutter_radio_player.dart
+++ b/lib/flutter_radio_player.dart
@@ -23,12 +23,13 @@ class FlutterRadioPlayer {
static Stream _metaDataStream;
Future init(String appName, String subTitle, String streamURL,
- String playWhenReady) async {
+ String playWhenReady,{ByteData coverImage}) async {
return await _channel.invokeMethod("initService", {
"appName": appName,
"subTitle": subTitle,
"streamURL": streamURL,
- "playWhenReady": playWhenReady
+ "playWhenReady": playWhenReady,
+ "coverImage": coverImage.buffer.asUint8List()
});
}
@@ -36,6 +37,10 @@ class FlutterRadioPlayer {
return await _channel.invokeMethod("play");
}
+ Future newPlay() async {
+ return await _channel.invokeMethod("newPlay");
+ }
+
Future pause() async {
return await _channel.invokeMethod("pause");
}
@@ -58,10 +63,18 @@ class FlutterRadioPlayer {
await _channel.invokeMethod("setVolume", {"volume": volume});
}
- Future setUrl(String streamUrl, String playWhenReady) async {
+ Future setTitle(String title, String subtitle) async {
+ await _channel.invokeMethod("setTitle", {
+ "title": title,
+ "subtitle": subtitle
+ });
+ }
+
+ Future setUrl(String streamUrl, String playWhenReady, ByteData coverImage) async {
await _channel.invokeMethod("setUrl", {
"playWhenReady": playWhenReady,
- "streamUrl": streamUrl
+ "streamUrl": streamUrl,
+ "coverImage": coverImage.buffer.asUint8List()
});
}
diff --git a/pubspec.lock b/pubspec.lock
index 9477cdd..fcfae0a 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -144,3 +144,4 @@ packages:
version: "2.0.8"
sdks:
dart: ">=2.9.0-14.0.dev <3.0.0"
+ flutter: ">=1.12.0 <2.0.0"