diff --git a/android/build.gradle b/android/build.gradle index 25d0ba4..e95458f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,7 +1,7 @@ buildscript { - ext.kotlin_version = '1.5.32' - ext.exo_player_version = '2.18.1' + ext.kotlin_version = '1.8.0' + ext.exo_player_version = '2.18.2' repositories { google() @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:7.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -29,7 +29,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -56,10 +56,12 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation "org.greenrobot:eventbus:3.3.1" - implementation 'com.google.code.gson:gson:2.8.9' + implementation 'com.google.code.gson:gson:2.10.1' implementation "androidx.multidex:multidex:2.0.1" - implementation "com.google.android.exoplayer:exoplayer:$exo_player_version" + implementation "com.google.android.exoplayer:exoplayer-core:$exo_player_version" + implementation "com.google.android.exoplayer:exoplayer-ui:$exo_player_version" + implementation "com.google.android.exoplayer:exoplayer-hls:$exo_player_version" implementation "com.google.android.exoplayer:extension-mediasession:$exo_player_version" } \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d45..94adc3a 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index f3d0e8a..32b25ba 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 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 ae11f3b..f575676 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 @@ -85,7 +85,6 @@ class FlutterRadioPlayerPlugin : FlutterPlugin, ActivityAware, MethodChannel.Met Log.i(TAG, "::: Attaching to FRP to FlutterEngine :::") this.context = context frpChannel = MethodChannel(binaryMessenger, METHOD_CHANNEL_NAME) - frpChannel?.setMethodCallHandler(this) val eventChannel = EventChannel(binaryMessenger, EVENT_CHANNEL_NAME) eventChannel.setStreamHandler(object : EventChannel.StreamHandler { @@ -121,8 +120,10 @@ class FlutterRadioPlayerPlugin : FlutterPlugin, ActivityAware, MethodChannel.Met isBound = false } } + Log.i(TAG, "Binding service...") + flutterPluginBinding?.applicationContext?.bindService(serviceIntent, serviceConnection!!, Context.BIND_AUTO_CREATE) - context.bindService(serviceIntent, serviceConnection!!, Context.BIND_AUTO_CREATE) + frpChannel?.setMethodCallHandler(this) } @Subscribe(threadMode = ThreadMode.MAIN) @@ -130,6 +131,7 @@ class FlutterRadioPlayerPlugin : FlutterPlugin, ActivityAware, MethodChannel.Met if (eventSink != null) { Log.d(TAG, "FRP Event data = $event") if (event.playbackStatus != null) { + // TODO reconsider unbinding service on stop because it might be too cumbersome to rebind it when resuming if (event.playbackStatus == FRP_STOPPED) { Log.i(TAG, "Service unbind....") isBound = false @@ -152,6 +154,7 @@ class FlutterRadioPlayerPlugin : FlutterPlugin, ActivityAware, MethodChannel.Met } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + Log.i(TAG, ":::: received method call: ${call.method} ::::") when (call.method) { "init_service" -> { if (isBound) { @@ -219,7 +222,7 @@ class FlutterRadioPlayerPlugin : FlutterPlugin, ActivityAware, MethodChannel.Met } "seek_source_to_index" -> { if (!isBound) { - result.error("FRP_008", "Failed to call prev_source", null) + result.error("FRP_008", "Failed to call seek_source_to_index", null) throw FRPException("FRPCoreService has not been initialized yet") } val sourceIndex: Int = call.argument("source_index") ?: 0 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 f3acdda..8b13789 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,404 +1 @@ -package me.sithiramunasinghe.flutter.flutter_radio_player.core -import android.app.Activity -import android.app.Notification -import android.app.PendingIntent -import android.app.Service -import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.Color -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.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.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 - - private var isBound = false - private val iBinder = LocalBinder() - private lateinit var playbackStatus: PlaybackStatus - private lateinit var dataSourceFactory: DefaultDataSourceFactory - private lateinit var localBroadcastManager: LocalBroadcastManager - - // context - private val context = this - 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 currentMetadata: String = "" - private var playerNotificationManager: PlayerNotificationManager? = null - - val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> - when (focusChange) { - AudioManager.AUDIOFOCUS_LOSS -> { - pause() - handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30)) - } - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { - pause() - } - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { - setVolume(0.1) - } - AudioManager.AUDIOFOCUS_GAIN -> { - setVolume(1.0) - play() - } - } - } - - // session keys - private val playbackNotificationId = 1025 - private val mediaSessionId = "streaming_audio_player_media_session" - private val playbackChannelId = "streaming_audio_player_channel_id" - - inner class LocalBinder : Binder() { - internal val service: StreamingCore - get() = this@StreamingCore - } - - /*=========================== - * Player APIS - *=========================== - */ - - 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 - } - - fun pause() { - logger.info("pausing audio...") - player?.playWhenReady = false - } - - fun isPlaying(): Boolean { - val isPlaying = this.playbackStatus == PlaybackStatus.PLAYING - logger?.info("is playing status: $isPlaying") - return isPlaying - } - - fun stop() { - logger.info("stopping audio $player ...") - player?.stop() - stopSelf() - isBound = false - } - - fun setVolume(volume: Double) { - logger.info("Changing volume to : $volume") - player?.volume = volume.toFloat() - } - - fun setUrl(streamUrl: String, playWhenReady: Boolean) { - logger.info("ReadyPlay status: $playWhenReady") - logger.info("Set stream URL: $streamUrl") - player?.prepare(buildMediaSource(dataSourceFactory, streamUrl)) - player?.playWhenReady = playWhenReady - } - - private var delayedStopRunnable = Runnable { -// stop() - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - - logger.info("Firing up service. (onStartCommand)...") - - localBroadcastManager = LocalBroadcastManager.getInstance(context) - - logger.info("LocalBroadCastManager Received...") - - // get details - val appName: String = intent?.getStringExtra("appName") ?: "Online-Radio" - currentMetadata = intent?.getStringExtra("subTitle") ?: "Live" - val streamUrl: String? = intent?.getStringExtra("streamUrl") - val playWhenReady: Boolean = intent?.getStringExtra("playWhenReady") ?: "false" == "true" - val primaryColor: Int = intent?.getIntExtra("primaryColor", Color.TRANSPARENT) ?: Color.TRANSPARENT - - if (streamUrl == null) { - logger.warning("Streaming service invoked without Stream URL! Shutting service down") - stop() - return START_NOT_STICKY - } - - player = SimpleExoPlayer.Builder(context).build() - - - // Gain Audio Focus - 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 - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - audioManager!!.requestAudioFocus(focusRequest!!) - } else { - audioManager!!.requestAudioFocus(afChangeListener, AudioEffect.CONTENT_TYPE_MUSIC, 0); - } - - // Build Media player - dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, appName)) - - val audioSource: MediaSource = buildMediaSource(dataSourceFactory, streamUrl!!) - - val playerEvents = object : Player.EventListener { - - override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { - playbackStatus = when (playbackState) { - Player.STATE_BUFFERING -> { - pushEvent(FLUTTER_RADIO_PLAYER_LOADING) - PlaybackStatus.LOADING - - } - Player.STATE_IDLE -> { - pushEvent(FLUTTER_RADIO_PLAYER_STOPPED) - PlaybackStatus.STOPPED - } - Player.STATE_READY -> { - setPlayWhenReady(playWhenReady) - } - else -> setPlayWhenReady(playWhenReady) - } - 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); - } - } - logger.info("onPlayerStateChanged: $playbackStatus") - } - - - override fun onPlayerError(error: ExoPlaybackException) { - pushEvent(FLUTTER_RADIO_PLAYER_ERROR) - playbackStatus = PlaybackStatus.ERROR - error.printStackTrace() - } - } - - // set exo player configs - player?.let { - it.addListener(playerEvents) - it.playWhenReady = playWhenReady - it.prepare(audioSource) - } - - // register our meta data listener - player?.addMetadataOutput { - currentMetadata = it.get(0).toString() - localBroadcastManager.sendBroadcast(broadcastMetaDataIntent.putExtra("meta_data", currentMetadata)) - playerNotificationManager?.invalidate() - } - - playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel( - context, - playbackChannelId, - R.string.channel_name, - R.string.channel_description, - playbackNotificationId, - object : PlayerNotificationManager.MediaDescriptionAdapter { - - override fun getCurrentContentTitle(player: Player): String { - return appName - } - - @Nullable - override fun createCurrentContentIntent(player: Player): PendingIntent { - var intent = Intent(this@StreamingCore, activity!!.javaClass) - var contentPendingIntent = PendingIntent.getActivity(this@StreamingCore, 0, intent, 0); - return contentPendingIntent; - } - - @Nullable - override fun getCurrentContentText(player: Player): String? { - val parsedMetadata = IcyMetadata(currentMetadata) - logger.info("ICY Metadata parsed, reading title") - return parsedMetadata.get("title") - } - - @Nullable - override fun getCurrentLargeIcon(player: Player, callback: PlayerNotificationManager.BitmapCallback): Bitmap? { - return null // OS will use the application icon. - } - - }, - object : PlayerNotificationManager.NotificationListener { - override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) { - logger.info("Notification Cancelled. Stopping player...") - stop() - } - - override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) { - logger.info("Attaching player as a foreground notification...") - startForeground(notificationId, notification) - } - - } - ) - - logger.info("Building Media Session and Player Notification.") - - val mediaSession = MediaSessionCompat(context, mediaSessionId) - mediaSession.isActive = true - - try { - mediaSessionConnector = MediaSessionConnector(mediaSession) - mediaSessionConnector?.setPlayer(player) - } catch (error: NoSuchMethodError) { - // We've had this error: No static method getLooper()Landroid/os/Looper; in class Lcom/google/android/exoplayer2/util/Util; or its super classes (declaration of 'com.google.android.exoplayer2.util.Util' appears in - logger.warning("mediaSessionConnector cant be set; Exoplayer requires the plugin to be built with Java 8. See https://github.com/google/ExoPlayer/issues/6378 for more details") - } - - if (primaryColor != Color.TRANSPARENT) { - playerNotificationManager!!.setColor(primaryColor) - playerNotificationManager!!.setColorized(true) - } - - playerNotificationManager!!.setUseStopAction(true) - playerNotificationManager!!.setFastForwardIncrementMs(0) - playerNotificationManager!!.setRewindIncrementMs(0) - playerNotificationManager!!.setUsePlayPauseActions(true) - playerNotificationManager!!.setUseNavigationActions(false) - playerNotificationManager!!.setUseNavigationActionsInCompactView(false) - - playerNotificationManager!!.setPlayer(player) - playerNotificationManager!!.setMediaSessionToken(mediaSession.sessionToken) - - playbackStatus = PlaybackStatus.PLAYING - - return START_REDELIVER_INTENT - } - - override fun onBind(intent: Intent?): IBinder? { - return iBinder - } - - override fun onDestroy() { - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - mediaSession?.release() - } - - mediaSessionConnector?.setPlayer(null) - playerNotificationManager?.setPlayer(null) - player?.release() - - super.onDestroy() - } - - override fun onAudioFocusChange(audioFocus: Int) { - when (audioFocus) { - - AudioManager.AUDIOFOCUS_GAIN -> { - player?.volume = 0.8f - play() - } - - AudioManager.AUDIOFOCUS_LOSS -> { - pause() - } - - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { - if (isPlaying()) { - pause() - } - } - - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { - if (isPlaying()) { - player?.volume = 0.1f - } - } - } - } - - /** - * Push events to local broadcaster service. - */ - private fun pushEvent(eventName: String) { - logger.info("Pushing Event: $eventName") - localBroadcastManager.sendBroadcast(Intent(broadcastActionName).putExtra("status", eventName)) - } - - /** - * Build the media source depending of the URL content type. - */ - private fun buildMediaSource(dataSourceFactory: DefaultDataSourceFactory, streamUrl: String): MediaSource { - - 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) - else -> { - throw IllegalStateException("Unsupported type: $type") - } - } - } - - private fun setPlayWhenReady(playWhenReady: Boolean): PlaybackStatus { - return if (playWhenReady) { - pushEvent(FLUTTER_RADIO_PLAYER_PLAYING) - PlaybackStatus.PLAYING - } else { - pushEvent(FLUTTER_RADIO_PLAYER_PAUSED) - PlaybackStatus.PAUSED - } - } - -} diff --git a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/services/FRPCoreService.kt b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/services/FRPCoreService.kt index b85f523..5033e28 100644 --- a/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/services/FRPCoreService.kt +++ b/android/src/main/kotlin/me/sithiramunasinghe/flutter/flutter_radio_player/core/services/FRPCoreService.kt @@ -10,7 +10,6 @@ import android.support.v4.media.session.MediaSessionCompat import com.google.android.exoplayer2.* import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -import com.google.android.exoplayer2.metadata.MetadataOutput import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource @@ -47,9 +46,7 @@ class FRPCoreService : Service(), PlayerNotificationManager.NotificationListener private val mediaSessionId = "flutter_radio_player_media_session_id" private val playbackChannelId = "flutter_radio_player_pb_channel_id" - // private var audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - - var playbackStatus = FRPPlaybackStatus.STOPPED + var playbackStatus = FRPPlaybackStatus.LOADING var currentMetaData: MediaMetadata? = null var mediaSourceList: List = emptyList() var useICYData: Boolean = false @@ -60,44 +57,11 @@ class FRPCoreService : Service(), PlayerNotificationManager.NotificationListener private val binder = LocalBinder() private var exoPlayer: ExoPlayer? = null private val eventBus: EventBus = EventBus.getDefault() - private var exoPlayerBuilder = ExoPlayer.Builder(context) private var mediaSessionConnector: MediaSessionConnector? = null private var playerNotificationManager: PlayerNotificationManager? = null private lateinit var mediaSession: MediaSessionCompat - override fun onCreate() { - - Log.i(TAG, "FlutterRadioPlayerService::onCreate") - - // build exoplayer - exoPlayer = exoPlayerBuilder - .setLooper(handler.looper) - .setAudioAttributes( - AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) - .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) - .build(), true - ) - .setLivePlaybackSpeedControl( - DefaultLivePlaybackSpeedControl.Builder() - .setFallbackMaxPlaybackSpeed(1.04f) - .build() - ) - .setHandleAudioBecomingNoisy(true) - .build() - - // exoplayer configuration - exoPlayer?.let { - it.addListener(FRPPlayerListener(this, exoPlayer, playerNotificationManager, eventBus)) - it.playWhenReady = false - } - - exoPlayer?.addAnalyticsListener(EventLogger()) - - Log.i(TAG, "::::: END FlutterRadioPlayerService::onCreate ::::") - } - override fun onDestroy() { Log.i(TAG, "::: onDestroy :::") @@ -106,10 +70,13 @@ class FRPCoreService : Service(), PlayerNotificationManager.NotificationListener mediaSession.release() } - if (exoPlayer != null) { - exoPlayer?.release() + exoPlayer?.release() + exoPlayer = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + stopForeground(false) } - mediaSessionConnector?.setPlayer(null) playerNotificationManager?.setPlayer(null) @@ -135,14 +102,16 @@ class FRPCoreService : Service(), PlayerNotificationManager.NotificationListener // set connector and player mediaSessionConnector = MediaSessionConnector(mediaSession) - mediaSessionConnector?.setPlayer(exoPlayer) playerNotificationManager?.apply { - Log.i(TAG, "Applying configurations...") + Log.i(TAG, ":::: Applying configurations... ::::") // default buttons - setUseStopAction(true) + // TODO allow developer to choose actions on player init instead of hardcoding them here + // ie. move true/false to Flutter side of the plugin and apply here + // TODO fix stopping (not pausing) and resuming player before enabling stop button + setUseStopAction(false) setUsePlayPauseActions(true) // next and prev buttons @@ -157,11 +126,10 @@ class FRPCoreService : Service(), PlayerNotificationManager.NotificationListener setUseFastForwardActionInCompactView(false) setUseRewindActionInCompactView(false) - setPlayer(exoPlayer) setMediaSessionToken(mediaSession.sessionToken) } - Log.i(TAG, ":::: END OF SERVICE ::::") + Log.i(TAG, ":::: END OF onStartCommand IN SERVICE ::::") return START_REDELIVER_INTENT } @@ -172,6 +140,47 @@ class FRPCoreService : Service(), PlayerNotificationManager.NotificationListener throw FRPException("Empty media sources") } + // TODO maybe find a better init location where the player will always be initialized correctly (onCreate wasn't called always) + // build exoplayer + if (exoPlayer == null) { + exoPlayer = ExoPlayer.Builder(context) + .setLooper(handler.looper) + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .build(), true + ) + .setLivePlaybackSpeedControl( + DefaultLivePlaybackSpeedControl.Builder() + .setFallbackMaxPlaybackSpeed(1.04f) + .build() + ) + .setRenderersFactory( + DefaultRenderersFactory(context) + .forceEnableMediaCodecAsynchronousQueueing() + ) + .setHandleAudioBecomingNoisy(true) + .build() + + // exoplayer configuration + exoPlayer?.let { + it.addListener( + FRPPlayerListener( + this, + exoPlayer, + playerNotificationManager, + eventBus + ) + ) + it.playWhenReady = false + } + + exoPlayer?.addAnalyticsListener(EventLogger()) + mediaSessionConnector?.setPlayer(exoPlayer) + playerNotificationManager?.setPlayer(exoPlayer) + } + this.mediaSourceList = sourceList.sortedByDescending { it.isPrimary } if (this.mediaSourceList.none { frpAudioSource -> frpAudioSource.isPrimary }) { @@ -179,58 +188,56 @@ class FRPCoreService : Service(), PlayerNotificationManager.NotificationListener } Log.i(TAG, "Current PlaybackStatus $playbackStatus") + Log.i(TAG, "Current Player state ${exoPlayer?.playbackState}") val defaultSource = this.mediaSourceList.firstOrNull { frp -> frp.isPrimary } - if (playbackStatus == FRPPlaybackStatus.PAUSED || playbackStatus == FRPPlaybackStatus.STOPPED) { + if (defaultSource != null) { + Log.i(TAG, "Default media item added to exoplayer...") + + val mediaUrl = Uri.parse(defaultSource.url) - if (defaultSource != null) { - Log.i(TAG, "Default media item added to exoplayer...") + val mediaBuilder = + MediaItem.Builder().setUri(mediaUrl).setLiveConfiguration( + MediaItem.LiveConfiguration.Builder() + .setMaxPlaybackSpeed(1.02f) + .build() + ) - val mediaUrl = Uri.parse(defaultSource.url) + if (defaultSource.isAcc == true) { + mediaBuilder.setMimeType(MimeTypes.AUDIO_AAC) + Log.d(TAG, "is an AAC media source") + } + + exoPlayer?.addMediaSource(0, buildMediaSource(mediaUrl, mediaBuilder.build())) + updateCurrentPlaying(defaultSource) + } + + mediaSourceList.filter { source -> !source.isPrimary }.forEach { frp -> + run { + Log.i(TAG, "Added media source ${frp.title} with url ${frp.url}") + + val mediaUrl = Uri.parse(frp.url) - val mediaBuilder = - MediaItem.Builder().setUri(mediaUrl).setLiveConfiguration( + val mediaBuilder = MediaItem.Builder() + .setUri(mediaUrl) + .setLiveConfiguration( MediaItem.LiveConfiguration.Builder() .setMaxPlaybackSpeed(1.02f) .build() ) - if (defaultSource.isAcc!!) { + if (frp.isAcc!!) { mediaBuilder.setMimeType(MimeTypes.AUDIO_AAC) - Log.d(TAG, "is an AAC media source") } - exoPlayer?.addMediaSource(0, buildMediaSource(mediaUrl, mediaBuilder.build())) - updateCurrentPlaying(defaultSource) - } - - mediaSourceList.filter { source -> !source.isPrimary }.forEach { frp -> - run { - Log.i(TAG, "Added media source ${frp.title} with url ${frp.url}") - - val mediaUrl = Uri.parse(frp.url) - - val mediaBuilder = MediaItem.Builder() - .setUri(mediaUrl) - .setLiveConfiguration( - MediaItem.LiveConfiguration.Builder() - .setMaxPlaybackSpeed(1.02f) - .build() - ) - - if (frp.isAcc!!) { - mediaBuilder.setMimeType(MimeTypes.AUDIO_AAC) - } - - exoPlayer?.addMediaSource(buildMediaSource(mediaUrl, mediaBuilder.build())) - } + exoPlayer?.addMediaSource(buildMediaSource(mediaUrl, mediaBuilder.build())) } - - Log.i(TAG, "Preparing player...") - exoPlayer?.prepare() } + Log.i(TAG, "Preparing player...") + exoPlayer?.prepare() + if (playDefault) { Log.i(TAG, "addMediaSources with default play") exoPlayer?.playWhenReady = true @@ -253,6 +260,11 @@ class FRPCoreService : Service(), PlayerNotificationManager.NotificationListener } fun pause() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_DETACH) + } else { + stopForeground(false) + } exoPlayer?.pause() } @@ -287,10 +299,15 @@ class FRPCoreService : Service(), PlayerNotificationManager.NotificationListener } fun seekToMediaItem(index: Int, playIfReady: Boolean) { + Log.i(TAG, "Seeking to media item, pos: $index...") + + Log.d(TAG, "playbackState ${exoPlayer?.playbackState}") + Log.d(TAG, "playbackLooper ${exoPlayer?.playbackLooper}") exoPlayer?.seekToDefaultPosition(index) exoPlayer?.apply { playWhenReady = playIfReady } + exoPlayer?.prepare() val currentMedia = mediaSourceList[exoPlayer?.currentMediaItemIndex!!] eventBus.post(FRPPlayerEvent(currentSource = updateCurrentPlaying(currentMedia))) } @@ -329,7 +346,8 @@ class FRPCoreService : Service(), PlayerNotificationManager.NotificationListener } return when (val type = Util.inferContentType(mediaUrl)) { - C.CONTENT_TYPE_HLS -> HlsMediaSource.Factory(defaultDataSource).createMediaSource(mediaItem) + C.CONTENT_TYPE_HLS -> HlsMediaSource.Factory(defaultDataSource) + .createMediaSource(mediaItem) C.CONTENT_TYPE_OTHER -> ProgressiveMediaSource.Factory(defaultDataSource) .createMediaSource(mediaItem) else -> { diff --git a/example/android/build.gradle b/example/android/build.gradle index a9ddbd9..d4c3e97 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.5.32' + ext.kotlin_version = '1.8.0' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:7.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 595fb86..6b66533 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/example/pubspec.lock b/example/pubspec.lock index 0ce46a4..bb69552 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,49 +5,56 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" flutter: @@ -59,7 +66,8 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" source: hosted version: "1.0.4" flutter_radio_player: @@ -68,45 +76,58 @@ packages: path: ".." relative: true source: path - version: "2.0.0" + version: "2.0.3" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" source: hosted version: "1.0.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted version: "1.8.2" sky_engine: @@ -118,51 +139,58 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.16" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" sdks: - dart: ">=2.17.0-0 <3.0.0" + dart: ">=2.18.0 <4.0.0" flutter: ">=2.5.0" diff --git a/ios/Classes/core/services/support/FRPPlayerEventHandler.swift b/ios/Classes/core/services/support/FRPPlayerEventHandler.swift index af1ec0d..5db31c0 100644 --- a/ios/Classes/core/services/support/FRPPlayerEventHandler.swift +++ b/ios/Classes/core/services/support/FRPPlayerEventHandler.swift @@ -15,18 +15,15 @@ class FRPPlayerEventHandler: NSObject { print("::::: EVENT HANDLER INIT ::::") } - static func handleMetaDataChanges(metaDetails: Array) { + static func handleMetaDataChanges(metaGroup: Array) { if (FRPCoreService.shared.useIcyData) { - metaDetails - .compactMap({ $0 as AVMetadataItem }) - .forEach({ meta in - print("Meta details \(meta)") - FRPCoreService.shared.currentMetaData = meta - if let nowPlayingTitle = meta.value { - FRPCoreService.shared.player.nowPlayingInfoController.set(keyValue: MediaItemProperty.albumTitle(nowPlayingTitle as? String)) - FRPNotificationUtil.shared.publish(eventData: FRPPlayerEvent(icyMetaDetails: nowPlayingTitle as? String)) - } - }) + let metadata = metaGroup.first?.items ?? [] + let title = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: .commonIdentifierTitle).first?.stringValue ?? "" + + if !title.isEmpty { + FRPCoreService.shared.player.nowPlayingInfoController.set(keyValue: MediaItemProperty.albumTitle(title)) + FRPNotificationUtil.shared.publish(eventData: FRPPlayerEvent(icyMetaDetails: title)) + } } } diff --git a/lib/flutter_radio_player.dart b/lib/flutter_radio_player.dart index dc2ece8..8e58011 100644 --- a/lib/flutter_radio_player.dart +++ b/lib/flutter_radio_player.dart @@ -37,6 +37,7 @@ class FlutterRadioPlayer { } _eventStream ??= _eventChannel.receiveBroadcastStream().map((event) => event); + _methodChannel.invokeMethod("init_service"); if (kDebugMode) { print("Initialized Event Channels: Completed"); } diff --git a/pubspec.yaml b/pubspec.yaml index 1419f3f..481b8ef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_radio_player description: Online Radio Player for Flutter which enable to play streaming URL. Supports Android and iOS as well as WearOs and watchOs -version: 2.0.0 +version: 2.2.0 homepage: "https://github.com/Sithira/FlutterRadioPlayer" environment: