diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index 96ffe242..9d5c26f5 100644 --- a/packages/player/android/build.gradle +++ b/packages/player/android/build.gradle @@ -4,7 +4,7 @@ version '1.0.4' buildscript { ext.kotlin_version = '1.9.20' - ext.media3_version = '1.4.1' + ext.media3_version = '1.5.1' repositories { google() mavenCentral() @@ -32,7 +32,7 @@ kapt { } android { - compileSdkVersion 34 + compileSdk = 35 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -49,7 +49,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1' implementation "androidx.media:media:1.7.0" implementation "org.jetbrains.kotlin:kotlin-reflect" //MEDIA3 DEPENDENCIES @@ -60,8 +60,11 @@ dependencies { implementation "androidx.media3:media3-ui:$media3_version" // implementation files('/Users/lucastonussi/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar') - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1' implementation "com.google.code.gson:gson:2.10.1" - +// api 'com.google.android.gms:play-services-cast-framework:22.0.0' + // CHROMECAST DEPENDENCIES + implementation 'androidx.mediarouter:mediarouter:1.7.0' + implementation 'androidx.media3:media3-cast:1.5.1' } diff --git a/packages/player/android/src/main/AndroidManifest.xml b/packages/player/android/src/main/AndroidManifest.xml index 9fd545b9..8827cbfd 100644 --- a/packages/player/android/src/main/AndroidManifest.xml +++ b/packages/player/android/src/main/AndroidManifest.xml @@ -8,6 +8,9 @@ + diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Cast.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Cast.kt new file mode 100644 index 00000000..ed885041 --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Cast.kt @@ -0,0 +1,329 @@ +package br.com.suamusica.player + +import android.content.Context +import android.util.Log +import androidx.media3.cast.SessionAvailabilityListener +import androidx.media3.common.util.UnstableApi +import androidx.mediarouter.media.MediaControlIntent +import androidx.mediarouter.media.MediaControlIntent.CATEGORY_LIVE_AUDIO +import androidx.mediarouter.media.MediaControlIntent.CATEGORY_LIVE_VIDEO +import androidx.mediarouter.media.MediaControlIntent.CATEGORY_REMOTE_PLAYBACK +import androidx.mediarouter.media.MediaRouteSelector +import androidx.mediarouter.media.MediaRouter +import androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_DISCONNECTED +import androidx.mediarouter.media.RemotePlaybackClient +import com.google.android.gms.cast.* +import com.google.android.gms.cast.framework.CastContext +import com.google.android.gms.cast.framework.CastState +import com.google.android.gms.cast.framework.CastStateListener +import com.google.android.gms.cast.framework.Session +import com.google.android.gms.cast.framework.SessionManager +import com.google.android.gms.cast.framework.SessionManagerListener +import com.google.android.gms.common.api.PendingResult +import com.google.android.gms.common.api.Status + + +@UnstableApi + +class CastManager( + castContext: CastContext, + context: Context, +) : + SessionAvailabilityListener, + CastStateListener, + Cast.Listener(), + SessionManagerListener, + PendingResult.StatusListener { + companion object { + const val TAG = "Chromecast" + } + + private var mediaRouter = MediaRouter.getInstance(context) + var isConnected = false + private var sessionManager: SessionManager? = null + private var mediaRouterCallback: MediaRouter.Callback? = null + private var onConnectCallback: (() -> Unit)? = null + private var onSessionEndedCallback: (() -> Unit)? = null + private var alreadyConnected = false + private var cookie: String = "" + + init { + castContext.addCastStateListener(this) + sessionManager = castContext.sessionManager + + //TODO: pode remover esse callback? + mediaRouterCallback = object : MediaRouter.Callback() { + override fun onRouteAdded(router: MediaRouter, route: MediaRouter.RouteInfo) { + super.onRouteAdded(router, route) + Log.d(TAG, "#NATIVE LOGS CAST ==> Route added: " + route.getName()) + } + + override fun onRouteRemoved(router: MediaRouter, route: MediaRouter.RouteInfo) { + super.onRouteRemoved(router, route) + Log.d(TAG, "#NATIVE LOGS CAST ==> Route removed: " + route.getName()) + } + + override fun onRouteChanged(router: MediaRouter, route: MediaRouter.RouteInfo) { + super.onRouteChanged(router, route) + Log.d(TAG, "#NATIVE LOGS CAST ==> Route changed: " + route.getName()) + } + + override fun onRouteSelected( + router: MediaRouter, + route: MediaRouter.RouteInfo, + reason: Int + ) { + Log.d( + TAG, + "#NATIVE LOGS CAST ==> Route selected: " + route.getName() + ", reason: " + reason + ) + } + + override fun onRouteUnselected( + router: MediaRouter, + route: MediaRouter.RouteInfo, + reason: Int + ) { + Log.d( + TAG, + "#NATIVE LOGS CAST ==> Route unselected: " + route.getName() + ", reason: " + reason + ) + } + } + + val selector: MediaRouteSelector.Builder = MediaRouteSelector.Builder() + .addControlCategory(CATEGORY_REMOTE_PLAYBACK) + //TODO: remover? + mediaRouterCallback?.let { + mediaRouter.addCallback( + selector.build(), it, + MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN + ) + } + } + +// fun discoveryCast(): List> { +// val casts = mutableListOf>() +// if (castContext.castState != CastState.NO_DEVICES_AVAILABLE) { +// mediaRouter.routes.forEach { +// if (it.deviceType == DEVICE_TYPE_TV && it.id.isNotEmpty()) { +// casts.add( +// mapOf( +// "name" to it.name, +// "id" to it.id, +// ) +// ) +// } +// } +// } +// return casts +// } + + fun connectToCast(idCast: String) { + val item = mediaRouter.routes.firstOrNull { + it.id.contains(idCast) + } + if (!isConnected) { + if (item != null) { + mediaRouter.selectRoute(item) + return + } + } else { + mediaRouter.unselect(UNSELECT_REASON_DISCONNECTED) + } + } + + fun disconnect() { + if (isConnected) { + sessionManager?.endCurrentSession(true) + onSessionEndedCallback?.invoke() + isConnected = false + } + } + +// private fun createQueueItem(mediaItem: MediaItem): MediaQueueItem { +// val mediaInfo = createMediaInfo(mediaItem) +// return MediaQueueItem.Builder(mediaInfo).build() +// } +// +// fun queueLoadCast(mediaItems: List) { +// val mediaQueueItems = mediaItems.map { mediaItem -> +// createQueueItem(mediaItem) +// } +// +// val cookieOk = cookie.replace("CloudFront-Policy=", "{\"CloudFront-Policy\": \"") +// .replace(";CloudFront-Key-Pair-Id=", "\", \"CloudFront-Key-Pair-Id\": \"") +// .replace(";CloudFront-Signature=", "\", \"CloudFront-Signature\": \"") + "\"}" +// +// +// val credentials = JSONObject().put("credentials", cookieOk) +// +// +// val request = sessionManager?.currentCastSession?.remoteMediaClient?.queueLoad( +// mediaQueueItems.toTypedArray(), +// player!!.currentMediaItemIndex, +// 1, +// player.currentPosition, +// credentials, +// ) +// +// request?.addStatusListener(this) +// } + +// fun loadMediaOld() { +// val media = player!!.currentMediaItem +// val url = media?.associatedMedia?.coverUrl!! +// +// val musictrackMetaData = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK) +// musictrackMetaData.putString(MediaMetadata.KEY_TITLE, media.associatedMedia?.name!!) +// musictrackMetaData.putString(MediaMetadata.KEY_ARTIST, media.associatedMedia?.author!!) +// musictrackMetaData.putString(MediaMetadata.KEY_ALBUM_TITLE, "albumName") +// musictrackMetaData.putString("images", url) +// +// media.associatedMedia?.coverUrl?.let { +// musictrackMetaData.addImage(WebImage(Uri.parse(it))) +// } +// +// val mediaInfo = +// MediaInfo.Builder(media.associatedMedia?.url!!) +// .setContentUrl(media.associatedMedia?.url!!) +// .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) +// .setMetadata(musictrackMetaData) +// .build() +// +// val cookieOk = cookie.replace("CloudFront-Policy=", "{\"CloudFront-Policy\": \"") +// .replace(";CloudFront-Key-Pair-Id=", "\", \"CloudFront-Key-Pair-Id\": \"") +// .replace(";CloudFront-Signature=", "\", \"CloudFront-Signature\": \"") + "\"}" +// +// val options = MediaLoadOptions.Builder() +//// .setPlayPosition(player.currentPosition) +// .setCredentials( +// cookieOk +// ) +// .build() +// +// val request = +// sessionManager?.currentCastSession?.remoteMediaClient?.load(mediaInfo, options) +// request?.addStatusListener(this) +// } + +// val remoteMediaClient: RemoteMediaClient? +// get() = sessionManager?.currentCastSession?.remoteMediaClient + + +// private fun createMediaInfo(mediaItem: MediaItem): MediaInfo { +// val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK).apply { +// putString(MediaMetadata.KEY_TITLE, mediaItem.associatedMedia?.name ?: "Title") +// putString(MediaMetadata.KEY_ARTIST, mediaItem.associatedMedia?.author ?: "Artist") +// putString( +// MediaMetadata.KEY_ALBUM_TITLE, +// mediaItem.associatedMedia?.albumTitle ?: "Album" +// ) +// +// mediaItem.associatedMedia?.coverUrl?.let { +// putString("images", it) +// } +// mediaItem.associatedMedia?.coverUrl?.let { coverUrl -> +// try { +// addImage(WebImage(Uri.parse(coverUrl.trim()))) +// } catch (e: Exception) { +// Log.e(TAG, "Failed to add cover image: ${e.message}") +// } +// } +// } +// +// return MediaInfo.Builder(mediaItem.associatedMedia?.url!!) +// .setContentUrl(mediaItem.associatedMedia?.url!!) +// .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) +// .setMetadata(metadata) +// .build() +// } + + //CAST STATE LISTENER + override fun onCastStateChanged(state: Int) { + Log.d( + TAG, + "#NATIVE LOGS CAST ==> RECEIVER UPDATE AVAILABLE ${CastState.toString(state)}" + ) + + if (alreadyConnected && state == CastState.NOT_CONNECTED) { + alreadyConnected = false + } + + if (!alreadyConnected) { + isConnected = state == CastState.CONNECTED + if (isConnected) { + alreadyConnected = true + onConnectCallback?.invoke() + } + } + } + + //SessionAvailabilityListener + override fun onCastSessionAvailable() { + Log.d(TAG, "#NATIVE LOGS CAST ==>- SessionAvailabilityListener: onCastSessionAvailable") + } + + override fun onCastSessionUnavailable() { + Log.d(TAG, "#NATIVE LOGS CAST ==>- SessionAvailabilityListener: onCastSessionUnavailable") + } + + //PendingResult.StatusListener + override fun onComplete(status: Status) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onComplete $status") + } + + + //SESSION MANAGER LISTENER + override fun onSessionEnded(p0: Session, p1: Int) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionEnded") + } + + override fun onSessionEnding(p0: Session) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionEnding") + } + + override fun onSessionResumeFailed(p0: Session, p1: Int) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResumeFailed") + } + + override fun onSessionResumed(p0: Session, p1: Boolean) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResumed") + } + + override fun onSessionResuming(p0: Session, p1: String) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResuming") + } + + override fun onSessionStartFailed(p0: Session, p1: Int) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionStartFailed $p0, $p1") + } + + override fun onSessionStarted(p0: Session, p1: String) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onCastSessionUnavailable") + onSessionEndedCallback?.invoke() + } + + override fun onSessionStarting(p0: Session) { + Log.d(TAG, "#NATIVE LOGS CAST ==> $p0 onSessionStarting") +// OnePlayerSingleton.toggleCurrentPlayer(true) + } + + override fun onSessionSuspended(p0: Session, p1: Int) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionSuspended") + } + +// var MediaItem.associatedMedia: Media? +// get() = mediaItemMediaAssociations[this] +// set(value) { +// mediaItemMediaAssociations[this] = value +// } + + fun setOnConnectCallback(callback: () -> Unit) { + onConnectCallback = callback + } + + fun setOnSessionEndedCallback(callback: () -> Unit) { + onSessionEndedCallback = callback + } +} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/CastOptionsProvider.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/CastOptionsProvider.kt new file mode 100644 index 00000000..eb6e9429 --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/CastOptionsProvider.kt @@ -0,0 +1,45 @@ +package br.com.suamusica.player + +import android.content.Context +import android.util.Log + +import com.google.android.gms.cast.framework.CastOptions +import com.google.android.gms.cast.framework.OptionsProvider +import com.google.android.gms.cast.framework.SessionProvider +import com.google.android.gms.cast.framework.media.CastMediaOptions +import com.google.android.gms.cast.framework.media.MediaIntentReceiver +import com.google.android.gms.cast.framework.media.NotificationOptions + +class CastOptionsProvider : OptionsProvider { + override fun getCastOptions(context: Context): CastOptions { + Log.d("Player","#NATIVE LOGS ==> CAST getCastOptions ") + +// val buttonActions = listOf( +// MediaIntentReceiver.ACTION_SKIP_NEXT, +// MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK, +// MediaIntentReceiver.ACTION_SKIP_PREV, +// MediaIntentReceiver.ACTION_STOP_CASTING +// ) + +// val compatButtonAction = intArrayOf(1, 3) +// val notificationOptions = +// NotificationOptions.Builder() +// .setActions(buttonActions, compatButtonAction) +// .setSkipStepMs(30000) +// .build() + val mediaOptions = CastMediaOptions.Builder() +// .setNotificationOptions(notificationOptions) + .setMediaSessionEnabled(false) + .setNotificationOptions(null) + .build() + return CastOptions.Builder() + .setReceiverApplicationId("A715FF7E") + .setCastMediaOptions(mediaOptions) + .build() + } + + override fun getAdditionalSessionProviders(context: Context): List? { + Log.d("Player","#NATIVE LOGS ==> CAST getCastOptions") + return null + } +} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt index 2a9611f2..2aea2305 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt @@ -5,6 +5,7 @@ import android.os.Build import android.os.Bundle import android.util.Log import android.view.KeyEvent +import androidx.media3.cast.CastPlayer import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Player.COMMAND_GET_TIMELINE @@ -12,6 +13,10 @@ import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM +import androidx.media3.common.Player.REPEAT_MODE_ALL +import androidx.media3.common.Player.REPEAT_MODE_OFF +import androidx.media3.common.Player.REPEAT_MODE_ONE +import androidx.media3.common.Player.STATE_IDLE import androidx.media3.common.util.UnstableApi import androidx.media3.session.CommandButton import androidx.media3.session.LibraryResult @@ -21,13 +26,13 @@ import androidx.media3.session.R.drawable import androidx.media3.session.SessionCommand import androidx.media3.session.SessionResult import br.com.suamusica.player.PlayerPlugin.Companion.DISABLE_REPEAT_MODE -import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE +import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.FAVORITE import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.ID_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT -import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY +import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.NEW_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST @@ -36,12 +41,14 @@ import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN import br.com.suamusica.player.PlayerPlugin.Companion.REORDER import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.SEEK_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.SET_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_IS_PLAYING import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_MEDIA_URI +import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -70,7 +77,7 @@ class MediaButtonEventHandler( } add(SessionCommand("notification_favoritar", Bundle.EMPTY)) add(SessionCommand("notification_desfavoritar", Bundle.EMPTY)) - add(SessionCommand("seek", session.token.extras)) + add(SessionCommand(SEEK_METHOD, session.token.extras)) add(SessionCommand("pause", Bundle.EMPTY)) add(SessionCommand("stop", Bundle.EMPTY)) add(SessionCommand("next", Bundle.EMPTY)) @@ -80,7 +87,7 @@ class MediaButtonEventHandler( add(SessionCommand(TOGGLE_SHUFFLE, Bundle.EMPTY)) add(SessionCommand(REPEAT_MODE, Bundle.EMPTY)) add(SessionCommand(DISABLE_REPEAT_MODE, Bundle.EMPTY)) - add(SessionCommand(ENQUEUE, session.token.extras)) + add(SessionCommand(ENQUEUE_METHOD, session.token.extras)) add(SessionCommand(REMOVE_ALL, Bundle.EMPTY)) add(SessionCommand(REORDER, session.token.extras)) add(SessionCommand(REMOVE_IN, session.token.extras)) @@ -94,6 +101,8 @@ class MediaButtonEventHandler( add(SessionCommand("onTogglePlayPause", Bundle.EMPTY)) add(SessionCommand(UPDATE_MEDIA_URI, session.token.extras)) add(SessionCommand(UPDATE_IS_PLAYING, session.token.extras)) + add(SessionCommand("cast", session.token.extras)) + add(SessionCommand("cast_next_media", session.token.extras)) }.build() val playerCommands = @@ -124,21 +133,28 @@ class MediaButtonEventHandler( buildIcons() } - if(customCommand.customAction == UPDATE_IS_PLAYING){ + if (customCommand.customAction == "cast") { + mediaService.castWithCastPlayer(args.getString("cast_id")) + } + + if (customCommand.customAction == UPDATE_IS_PLAYING) { buildIcons() } - if (customCommand.customAction == "seek") { - mediaService.seek(args.getLong("position"), args.getBoolean("playWhenReady")) + if (customCommand.customAction == SEEK_METHOD) { + val position = args.getLong("position") + val playWhenReady = args.getBoolean("playWhenReady") + session.player.seekTo(position) + session.player.playWhenReady = playWhenReady } if (customCommand.customAction == FAVORITE) { - val isFavorite = args.getBoolean(IS_FAVORITE_ARGUMENT) + val isFavorite = args.getBoolean(IS_FAVORITE_ARGUMENT) val mediaItem = session.player.currentMediaItem!! updateFavoriteMetadata( session.player, session.player.currentMediaItemIndex, mediaItem, - isFavorite, + isFavorite, ) buildIcons() } @@ -160,9 +176,15 @@ class MediaButtonEventHandler( mediaService.reorder(oldIndex, newIndex, positionsList) } + if (customCommand.customAction == "onTogglePlayPause") { - mediaService.togglePlayPause() + if (session.player.isPlaying) { + session.player.pause() + } else { + session.player.play() + } } + if (customCommand.customAction == TOGGLE_SHUFFLE) { // val list = args.getSerializable("list",ArrayList>()::class.java) val json = args.getString(POSITIONS_LIST) @@ -172,44 +194,91 @@ class MediaButtonEventHandler( mediaService.toggleShuffle(positionsList) } if (customCommand.customAction == REPEAT_MODE) { - mediaService.repeatMode() + session.player.let { + when (it.repeatMode) { + REPEAT_MODE_OFF -> { + it.repeatMode = REPEAT_MODE_ALL + } + + REPEAT_MODE_ONE -> { + it.repeatMode = REPEAT_MODE_OFF + } + + else -> { + it.repeatMode = REPEAT_MODE_ONE + } + } + } } if (customCommand.customAction == DISABLE_REPEAT_MODE) { mediaService.disableRepeatMode() } if (customCommand.customAction == "stop") { - mediaService.stop() + session.player.stop() } if (customCommand.customAction == "play") { - val shouldPrepare = args.getBoolean("shouldPrepare") - mediaService.play(shouldPrepare) + if (session.player.playbackState == STATE_IDLE) { + session.player.prepare() + } + session.player.play() } + if (customCommand.customAction == SET_REPEAT_MODE) { val mode = args.getString("mode") - mediaService.setRepeatMode(mode ?:"") + val convertedMode = when (mode) { + "off" -> REPEAT_MODE_OFF + "one" -> REPEAT_MODE_ONE + "all" -> REPEAT_MODE_ALL + else -> REPEAT_MODE_OFF + } + if (session.player is CastPlayer) { + playerChangeNotifier?.onRepeatChanged(convertedMode) + }else { + session.player.repeatMode = convertedMode + } } + + if (customCommand.customAction == "cast_next_media") { + val json = args.getString("media") + val gson = GsonBuilder().create() + val mediaListType = object : TypeToken() {}.type + val media: Media = gson.fromJson(json, mediaListType) + session.player.setMediaItem(mediaService.createMediaItem(media)) + } + if (customCommand.customAction == PLAY_FROM_QUEUE_METHOD) { - mediaService.playFromQueue( - args.getInt(POSITION_ARGUMENT), args.getLong(TIME_POSITION_ARGUMENT), - args.getBoolean( - LOAD_ONLY - ), - ) + if (session.player is CastPlayer) { + PlayerSingleton.getMediaFromQueue(args.getInt(POSITION_ARGUMENT)) + } else { + mediaService.playFromQueue( + args.getInt(POSITION_ARGUMENT), args.getLong(TIME_POSITION_ARGUMENT), + args.getBoolean( + LOAD_ONLY_ARGUMENT + ), + ) + } } if (customCommand.customAction == "notification_previous" || customCommand.customAction == "previous") { - if(session.player.hasPreviousMediaItem()){ - session.player.seekToPreviousMediaItem() - }else{ - session.player.seekToPrevious() + if (session.player is CastPlayer) { + PlayerSingleton.getPreviousMedia() + } else { + if (session.player.hasPreviousMediaItem()) { + session.player.seekToPreviousMediaItem() + } else { + session.player.seekToPrevious() + } } - mediaService.shouldNotifyTransition = true } if (customCommand.customAction == "notification_next" || customCommand.customAction == "next") { - mediaService.shouldNotifyTransition = true - session.player.seekToNextMediaItem() + if (session.player is CastPlayer) { + PlayerSingleton.getNextMedia() + } else { + session.player.seekToNextMediaItem() + } } + if (customCommand.customAction == "pause") { - mediaService.pause() + session.player.pause() } if (customCommand.customAction == UPDATE_MEDIA_URI) { @@ -242,14 +311,11 @@ class MediaButtonEventHandler( } } PlayerSingleton.favorite(isFavorite) -// } } if (customCommand.customAction == "ads_playing") { -// mediaService.player?.pause() -// mediaService.adsPlaying() mediaService.removeNotification() } - if (customCommand.customAction == ENQUEUE) { + if (customCommand.customAction == ENQUEUE_METHOD) { val json = args.getString("json") val gson = GsonBuilder().create() val mediaListType = object : TypeToken>() {}.type @@ -258,7 +324,6 @@ class MediaButtonEventHandler( mediaService.enqueue( mediaList, args.getBoolean("autoPlay"), - args.getBoolean("shouldNotifyTransition"), ) } return Futures.immediateFuture( @@ -286,7 +351,7 @@ class MediaButtonEventHandler( fun buildIcons() { val isFavorite = - mediaService.player?.currentMediaItem?.mediaMetadata?.extras?.getBoolean( + mediaService.smPlayer?.currentMediaItem?.mediaMetadata?.extras?.getBoolean( IS_FAVORITE_ARGUMENT ) ?: false @@ -371,9 +436,9 @@ class MediaButtonEventHandler( KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { Log.d("Player", "Player: Key Code : PlayPause") - if(session.player.isPlaying){ + if (session.player.isPlaying) { PlayerSingleton.pause() - }else{ + } else { PlayerSingleton.play() } } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt index 5d270ba7..0fee0224 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt @@ -1,31 +1,21 @@ package br.com.suamusica.player -import android.app.ActivityManager -import android.app.NotificationManager +import PlayerSwitcher import android.app.PendingIntent import android.app.Service import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Handler -import android.support.v4.media.session.PlaybackStateCompat -import androidx.core.app.NotificationManagerCompat +import androidx.media3.cast.CastPlayer +import androidx.media3.cast.DefaultMediaItemConverter +import androidx.media3.cast.MediaItemConverter import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.PlaybackException -import androidx.media3.common.PlaybackParameters -import androidx.media3.common.Player -import androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK -import androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION -import androidx.media3.common.Player.MediaItemTransitionReason import androidx.media3.common.Player.REPEAT_MODE_ALL import androidx.media3.common.Player.REPEAT_MODE_OFF -import androidx.media3.common.Player.REPEAT_MODE_ONE -import androidx.media3.common.Player.STATE_ENDED -import androidx.media3.common.Timeline import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util @@ -38,25 +28,21 @@ import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder -import androidx.media3.exoplayer.source.preload.BasePreloadManager import androidx.media3.session.CacheBitmapLoader import androidx.media3.session.CommandButton import androidx.media3.session.DefaultMediaNotificationProvider -import androidx.media3.session.LibraryResult -import androidx.media3.session.MediaConstants import androidx.media3.session.MediaController -import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService -import androidx.media3.session.SessionError -import br.com.suamusica.player.PlayerPlugin.Companion.FALLBACK_URL +import br.com.suamusica.player.PlayerPlugin.Companion.FALLBACK_URL_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.cookie import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory +import com.google.android.gms.cast.MediaQueueItem +import com.google.android.gms.cast.framework.CastContext import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -64,12 +50,11 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import org.json.JSONObject import java.io.File import java.util.Collections -import java.util.WeakHashMap -import java.util.concurrent.atomic.AtomicBoolean -const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" + const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 @UnstableApi @@ -83,36 +68,70 @@ class MediaService : MediaSessionService() { lateinit var mediaSession: MediaSession private var mediaController: ListenableFuture? = null - private val uAmpAudioAttributes = - AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA) - .build() + private val uAmpAudioAttributes = AudioAttributes.Builder() + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .setUsage(C.USAGE_MEDIA) + .build() - var player: ExoPlayer? = null + var playerSwitcher: PlayerSwitcher? = null + var exoPlayer: ExoPlayer? = null - private var progressTracker: ProgressTracker? = null + var castPlayer: CastPlayer? = null private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader private lateinit var mediaButtonEventHandler: MediaButtonEventHandler private var shuffleOrder: DefaultShuffleOrder? = null - - private var seekToLoadOnly: Boolean = false -// private var enqueueLoadOnly: Boolean = false - private var shuffledIndices = mutableListOf() private var autoPlay: Boolean = true - var shouldNotifyTransition: Boolean = true - private val channel = Channel>(Channel.BUFFERED) private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - private val mediaItemMediaAssociations = WeakHashMap() + + //CAST + private var cast: CastManager? = null + private var castContext: CastContext? = null + + val smPlayer get() = exoPlayer override fun onCreate() { super.onCreate() mediaButtonEventHandler = MediaButtonEventHandler(this) - - - player = ExoPlayer.Builder(this).build().apply { + castContext = CastContext.getSharedInstance(this) + + //TODO: nao cai aqui, e o que resolveu (justAudio) foi o setContentType do uAmpAudioAttributes + + // val mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager + + // val audioFocusListener = OnAudioFocusChangeListener { focusChange -> + // Log.d("onAudioFocusChangeTeste", focusChange.toString()) + // when (focusChange) { + // AudioManager.AUDIOFOCUS_LOSS -> { + // Log.d("onAudioFocusChangeTeste", "AUDIOFOCUS_LOSS") + // pause() + // } + // AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + // Log.d("onAudioFocusChangeTeste", "AUDIOFOCUS_LOSS_TRANSIENT") + // pause() + // } + // AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // Log.d("onAudioFocusChangeTeste", "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK") + // smPlayer?.volume = 0.2f + // } + // AudioManager.AUDIOFOCUS_GAIN -> { + // Log.d("onAudioFocusChangeTeste", "AUDIOFOCUS_GAIN") + // smPlayer?.volume = 1.0f + // play() + // } + // } + // } + + // mAudioManager.requestAudioFocus( + // audioFocusListener, + // AudioManager.STREAM_MUSIC, + // AudioManager.AUDIOFOCUS_GAIN + // ) + + + exoPlayer = ExoPlayer.Builder(this).build().apply { setAudioAttributes(uAmpAudioAttributes, true) - addListener(playerEventListener()) setWakeMode(C.WAKE_MODE_NETWORK) setHandleAudioBecomingNoisy(true) preloadConfiguration = ExoPlayer.PreloadConfiguration( @@ -123,7 +142,7 @@ class MediaService : MediaSessionService() { dataSourceBitmapLoader = DataSourceBitmapLoader(applicationContext) - player?.let { + exoPlayer?.let { mediaSession = MediaSession.Builder(this, it) .setBitmapLoader(CacheBitmapLoader(dataSourceBitmapLoader)) .setCallback(mediaButtonEventHandler) @@ -166,12 +185,53 @@ class MediaService : MediaSessionService() { } }) } + playerSwitcher = PlayerSwitcher(exoPlayer!!, mediaButtonEventHandler) + castContext?.let { + cast = CastManager(it, this) + } + } + + fun castWithCastPlayer(castId: String?) { + if (cast?.isConnected == true) { + cast?.disconnect() + return + } + val items = smPlayer?.getAllMediaItems() + if (!items.isNullOrEmpty()) { + cast?.connectToCast(castId!!) + cast?.setOnConnectCallback { + val index = smPlayer?.currentMediaItemIndex ?: 0 + val currentPosition: Long = smPlayer?.currentPosition ?: 0 + //TODO: verificar playback error do exoplayer ao conectar castPlayer + castPlayer = CastPlayer(castContext!!, CustomMediaItemConverter()) + mediaSession.player = castPlayer!! + playerSwitcher?.setCurrentPlayer( + castPlayer!!, + castContext?.sessionManager?.currentCastSession?.remoteMediaClient + ) + smPlayer?.setMediaItem(items[index]) + smPlayer?.seekTo(currentPosition) + smPlayer?.prepare() + smPlayer?.play() + } + + cast?.setOnSessionEndedCallback { + val currentPosition = smPlayer?.currentPosition ?: 0L + val index = smPlayer?.currentMediaItemIndex ?: 0 + exoPlayer?.let { + mediaSession.player = it + playerSwitcher?.setCurrentPlayer(it) + smPlayer?.prepare() + smPlayer?.seekTo(index, currentPosition) + } + } + } } + //TODO: testar se vai dar o erro de startForeground no caso de audioAd fun removeNotification() { Log.d("Player", "removeNotification") - player?.stop() -// NotificationManagerCompat.from(applicationContext).cancelAll() + smPlayer?.stop() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -199,10 +259,9 @@ class MediaService : MediaSessionService() { ): MediaSession = mediaSession override fun onTaskRemoved(rootIntent: Intent?) { - player?.clearMediaItems() + smPlayer?.clearMediaItems() Log.d(TAG, "onTaskRemoved") - player?.stop() - stopTrackingProgress() + smPlayer?.stop() stopSelf() super.onTaskRemoved(rootIntent) } @@ -222,55 +281,59 @@ class MediaService : MediaSessionService() { } private fun releasePossibleLeaks() { - player?.release() + smPlayer?.release() mediaSession.release() mediaController = null } - private fun isServiceRunning(): Boolean { - val manager = getSystemService(ACTIVITY_SERVICE) as ActivityManager - for (service in manager.getRunningServices(Int.MAX_VALUE)) { - if ("br.com.suamusica.player.MediaService" == service.service.className) { - return true - } + class CustomMediaItemConverter : MediaItemConverter { + override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem { + val queueItem = DefaultMediaItemConverter().toMediaQueueItem(mediaItem) + queueItem.Writer().setCustomData(JSONObject().put("credentials", cookie)) + return queueItem } - return false + + override fun toMediaItem(mediaQueueItem: MediaQueueItem): MediaItem { + return DefaultMediaItemConverter().toMediaItem(mediaQueueItem) + } + } fun updateMediaUri(index: Int, uri: String?) { -// if (index != player?.currentMediaItemIndex) { - val media = player?.getMediaItemAt(index) + val media = smPlayer?.getMediaItemAt(index) media?.associatedMedia?.let { - player?.removeMediaItem(index) - player?.addMediaSource( + smPlayer?.removeMediaItem(index) + smPlayer?.addMediaSource( index, prepare( cookie, it, - uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL) ?: "" + uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL_ARGUMENT) ?: "" ) ) -// player?.prepare() } -// } } fun toggleShuffle(positionsList: List>) { - player?.shuffleModeEnabled = !(player?.shuffleModeEnabled ?: false) - player?.shuffleModeEnabled?.let { + smPlayer?.shuffleModeEnabled = !(smPlayer?.shuffleModeEnabled ?: false) + smPlayer?.shuffleModeEnabled?.let { if (it) { - shuffledIndices.clear() + PlayerSingleton.shuffledIndices.clear() for (e in positionsList) { - shuffledIndices.add(e["originalPosition"] ?: 0) + PlayerSingleton.shuffledIndices.add(e["originalPosition"] ?: 0) } shuffleOrder = DefaultShuffleOrder( - shuffledIndices.toIntArray(), + PlayerSingleton.shuffledIndices.toIntArray(), System.currentTimeMillis() ) Log.d( TAG, - "toggleShuffle - shuffleOrder is null: ${shuffleOrder == null} | shuffledIndices: ${shuffledIndices.size} - ${player?.mediaItemCount}" + "toggleShuffle - shuffledIndices: ${PlayerSingleton.shuffledIndices.size}" ) - player!!.setShuffleOrder(shuffleOrder!!) + if(mediaSession.player !is CastPlayer){ + shuffleOrder?.let { shuffleOrder -> + smPlayer?.setShuffleOrder(shuffleOrder) + } + } } playerChangeNotifier?.onShuffleModeEnabled(it) } @@ -295,21 +358,14 @@ class MediaService : MediaSessionService() { fun enqueue( medias: List, autoPlay: Boolean, - shouldNotifyTransition: Boolean, ) { - Log.d( - TAG, - "enqueue: mediaItemCount: ${player?.mediaItemCount} | autoPlay: $autoPlay" - ) this.autoPlay = autoPlay - this.shouldNotifyTransition = shouldNotifyTransition - if (player?.mediaItemCount == 0) { - player?.playWhenReady = autoPlay + if (smPlayer?.mediaItemCount == 0) { + smPlayer?.playWhenReady = autoPlay } -// enqueueLoadOnly = autoPlay - android.util.Log.d( - "#NATIVE LOGS ==>", - "enqueue $autoPlay | mediaItemCount: ${player?.mediaItemCount} | shouldNotifyTransition: $shouldNotifyTransition" + Log.d( + TAG, + "#NATIVE LOGS MEDIA SERVICE ==> enqueue $autoPlay | mediaItemCount: ${smPlayer?.mediaItemCount}" ) addToQueue(medias) } @@ -320,19 +376,22 @@ class MediaService : MediaSessionService() { for (i in medias.indices) { mediaSources.add(prepare(cookie, medias[i], "")) } - player?.addMediaSources(mediaSources) - player?.prepare() -// PlayerSingleton.playerChangeNotifier?.notifyItemTransition("createMediaSource") -// playerChangeNotifier?.currentMediaIndex( -// currentIndex(), -// "createMediaSource", -// ) - } - if(shouldNotifyTransition){ - playerChangeNotifier?.notifyItemTransition("Enqueue - createMediaSource") + smPlayer?.addMediaSources(mediaSources) + smPlayer?.prepare() } } + fun createMediaItem(media: Media, uri: Uri? = null): MediaItem { + val metadata = buildMetaData(media) + return MediaItem.Builder() + .setMediaId(media.id.toString()) + .setUri(uri ?: Uri.parse(media.url)) + .setMediaMetadata(metadata) + .setMimeType("audio/mpeg") + .build() + .also { it.associatedMedia = media } + } + private fun prepare(cookie: String, media: Media, urlToPrepare: String): MediaSource { val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setReadTimeoutMs(15 * 1000) @@ -340,19 +399,17 @@ class MediaService : MediaSessionService() { dataSourceFactory.setUserAgent(userAgent) dataSourceFactory.setAllowCrossProtocolRedirects(true) dataSourceFactory.setDefaultRequestProperties(mapOf("Cookie" to cookie)) - val metadata = buildMetaData(media) + val uri = if (urlToPrepare.isEmpty()) { val url = media.url if (url.startsWith("/")) Uri.fromFile(File(url)) else Uri.parse(url) } else { Uri.parse(urlToPrepare) } - val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata) - .setMediaId(media.id.toString()).build() - mediaItem.associatedMedia = media - @C.ContentType val type = Util.inferContentType(uri) - return when (type) { + val mediaItem = createMediaItem(media, uri) + + return when (@C.ContentType val type = Util.inferContentType(uri)) { C.CONTENT_TYPE_HLS -> { HlsMediaSource.Factory(dataSourceFactory) .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) @@ -382,16 +439,18 @@ class MediaService : MediaSessionService() { newIndex: Int, positionsList: List> ) { - if (player?.shuffleModeEnabled == true) { - val list = shuffledIndices.ifEmpty { - positionsList.map { it["originalPosition"] ?: 0 }.toMutableList() + if (mediaSession.player !is CastPlayer) { + if (smPlayer?.shuffleModeEnabled == true) { + val list = PlayerSingleton.shuffledIndices.ifEmpty { + positionsList.map { it["originalPosition"] ?: 0 }.toMutableList() + } + Collections.swap(list, oldIndex, newIndex) + shuffleOrder = + DefaultShuffleOrder(list.toIntArray(), System.currentTimeMillis()) + smPlayer?.setShuffleOrder(shuffleOrder!!) + } else { + smPlayer?.moveMediaItem(oldIndex, newIndex) } - Collections.swap(list, oldIndex, newIndex) - shuffleOrder = - DefaultShuffleOrder(list.toIntArray(), System.currentTimeMillis()) - player?.setShuffleOrder(shuffleOrder!!) - } else { - player?.moveMediaItem(oldIndex, newIndex) } } @@ -399,45 +458,27 @@ class MediaService : MediaSessionService() { val sortedIndexes = indexes.sortedDescending() if (sortedIndexes.isNotEmpty()) { sortedIndexes.forEach { - player?.removeMediaItem(it) - if (shuffledIndices.isNotEmpty()) { - shuffledIndices.removeAt( - shuffledIndices.indexOf( - player?.currentMediaItemIndex ?: 0 + smPlayer?.removeMediaItem(it) + if (PlayerSingleton.shuffledIndices.isNotEmpty()) { + PlayerSingleton.shuffledIndices.removeAt( + PlayerSingleton.shuffledIndices.indexOf( + smPlayer?.currentMediaItemIndex ?: 0 ) ) } } } - if (player?.shuffleModeEnabled == true) { + if (smPlayer?.shuffleModeEnabled == true) { shuffleOrder = DefaultShuffleOrder( - shuffledIndices.toIntArray(), + PlayerSingleton.shuffledIndices.toIntArray(), System.currentTimeMillis() ) - player?.setShuffleOrder(shuffleOrder!!) + smPlayer?.setShuffleOrder(shuffleOrder!!) } } fun disableRepeatMode() { - player?.repeatMode = REPEAT_MODE_OFF - } - - fun repeatMode() { - player?.let { - when (it.repeatMode) { - REPEAT_MODE_OFF -> { - it.repeatMode = REPEAT_MODE_ALL - } - - REPEAT_MODE_ONE -> { - it.repeatMode = REPEAT_MODE_OFF - } - - else -> { - it.repeatMode = REPEAT_MODE_ONE - } - } - } + smPlayer?.repeatMode = REPEAT_MODE_OFF } private fun buildMetaData(media: Media): MediaMetadata { @@ -445,10 +486,11 @@ class MediaService : MediaSessionService() { val bundle = Bundle() bundle.putBoolean(IS_FAVORITE_ARGUMENT, media.isFavorite ?: false) - bundle.putString(FALLBACK_URL, media.fallbackUrl) + bundle.putString(FALLBACK_URL_ARGUMENT, media.fallbackUrl) metadataBuilder.apply { setAlbumTitle(media.name) setArtist(media.author) + //TODO: verificar cover sem internet e null e empty setArtworkUri(Uri.parse(media.bigCoverUrl)) setArtist(media.author) setTitle(media.name) @@ -459,256 +501,30 @@ class MediaService : MediaSessionService() { return metadata } - fun play(shouldPrepare: Boolean = false) { - performAndEnableTracking { - if (shouldPrepare) { - player?.prepare() - } - player?.play() - } - } - - fun setRepeatMode(mode: String) { - player?.repeatMode = when (mode) { - "off" -> REPEAT_MODE_OFF - "one" -> REPEAT_MODE_ONE - "all" -> REPEAT_MODE_ALL - else -> REPEAT_MODE_OFF - } - } - fun playFromQueue(position: Int, timePosition: Long, loadOnly: Boolean = false) { - player?.playWhenReady = !loadOnly - - if (loadOnly) { - seekToLoadOnly = true - } - - player?.seekTo( - if (player?.shuffleModeEnabled == true) shuffledIndices[position] else position, + smPlayer?.playWhenReady = !loadOnly + PlayerSingleton.shouldNotifyTransition = smPlayer?.playWhenReady ?: false + smPlayer?.seekTo( + if (smPlayer?.shuffleModeEnabled == true) PlayerSingleton.shuffledIndices[position] else position, timePosition, ) + if (!loadOnly) { - player?.prepare() - playerChangeNotifier?.notifyItemTransition("playFromQueue") + smPlayer?.prepare() } } fun removeAll() { - player?.stop() - player?.clearMediaItems() + smPlayer?.stop() + smPlayer?.clearMediaItems() } fun seek(position: Long, playWhenReady: Boolean) { - player?.seekTo(position) - player?.playWhenReady = playWhenReady - } - - fun pause() { - performAndDisableTracking { - player?.pause() - } - } - - fun stop() { - performAndDisableTracking { - player?.stop() - } - } - - fun togglePlayPause() { - if (player?.isPlaying == true) { - pause() - } else { - play() - } + smPlayer?.seekTo(position) + smPlayer?.playWhenReady = playWhenReady } private fun releaseAndPerformAndDisableTracking() { - performAndDisableTracking { - player?.stop() - } - } - - - private fun notifyPositionChange() { - var position = player?.currentPosition ?: 0L - val duration = player?.duration ?: 0L - position = if (position > duration) duration else position - playerChangeNotifier?.notifyPositionChange(position, duration) - } - - private fun startTrackingProgress() { - if (progressTracker != null) { - return - } - this.progressTracker = ProgressTracker(Handler()) - } - - private fun stopTrackingProgress() { - progressTracker?.stopTracking() - progressTracker = null - } - - private fun stopTrackingProgressAndPerformTask(callable: () -> Unit) { - if (progressTracker != null) { - progressTracker!!.stopTracking(callable) - } else { - callable() - } - progressTracker = null - } - - private fun performAndEnableTracking(callable: () -> Unit) { - callable() - startTrackingProgress() - } - - private fun performAndDisableTracking(callable: () -> Unit) { - callable() - stopTrackingProgress() - } - - fun currentIndex(): Int { - val position = if (player?.shuffleModeEnabled == true) { - shuffledIndices.indexOf( - player?.currentMediaItemIndex ?: 0 - ) - } else { - player?.currentMediaItemIndex ?: 0 - } - return position - } - - private fun playerEventListener(): Player.Listener { - return object : Player.Listener { - override fun onPositionDiscontinuity( - oldPosition: Player.PositionInfo, - newPosition: Player.PositionInfo, - reason: Int - ) { - if (reason == DISCONTINUITY_REASON_SEEK) { - playerChangeNotifier?.notifySeekEnd() - } - } - - override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - playerChangeNotifier?.notifyPlaying(isPlaying) - if (isPlaying) { -// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PLAYING) - startTrackingProgress() - } else { -// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PAUSED) - stopTrackingProgressAndPerformTask {} - } - } - - override fun onMediaItemTransition( - mediaItem: MediaItem?, - reason: @MediaItemTransitionReason Int - ) { - super.onMediaItemTransition(mediaItem, reason) - Log.d(TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason") - if ((player?.mediaItemCount ?: 0) > 0) { - playerChangeNotifier?.currentMediaIndex( - currentIndex(), - "onMediaItemTransition", - ) - } - mediaButtonEventHandler.buildIcons() - if(reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED || !shouldNotifyTransition){ - return - } - playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: ${reason} | shouldNotifyTransition: ${shouldNotifyTransition}") - shouldNotifyTransition = false - } - - var lastState = PlaybackStateCompat.STATE_NONE - 1 - - override fun onPlaybackStateChanged(playbackState: @Player.State Int) { - super.onPlaybackStateChanged(playbackState) - if (lastState != playbackState) { - lastState = playbackState - playerChangeNotifier?.notifyStateChange(playbackState) - } - - if (playbackState == STATE_ENDED) { - stopTrackingProgressAndPerformTask {} - } - Log.d(TAG, "##onPlaybackStateChanged $playbackState") - } - - override fun onPlayerErrorChanged(error: PlaybackException?) { - super.onPlayerErrorChanged(error) - Log.d(TAG, "##onPlayerErrorChanged ${error}") - - } - - override fun onRepeatModeChanged(repeatMode: @Player.RepeatMode Int) { - super.onRepeatModeChanged(repeatMode) - playerChangeNotifier?.onRepeatChanged(repeatMode) - } - - override fun onPlayerError(error: PlaybackException) { - android.util.Log.d( - "#NATIVE LOGS ==>", - "onPlayerError cause ${error.cause.toString()}" - ) - - playerChangeNotifier?.notifyError( - if (error.cause.toString() - .contains("Permission denied") - ) "Permission denied" else error.message - ) - } - - override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { - } - } - } - - var MediaItem.associatedMedia: Media? - get() = mediaItemMediaAssociations[this] - set(value) { - mediaItemMediaAssociations[this] = value - } - - private inner class ProgressTracker(val handler: Handler) : Runnable { - private val shutdownRequest = AtomicBoolean(false) - private var shutdownTask: (() -> Unit)? = null - - init { - handler.post(this) - } - - override fun run() { - val position = player?.currentPosition ?: 0L - val duration = player?.duration ?: 0L - - if (duration > 0 && position >= duration - 800) { - playerChangeNotifier?.notifyStateChange(STATE_ENDED) - Log.d(TAG, "#NATIVE LOGS ==> Notifying COMPLETED print ONLY") - } - - notifyPositionChange() - - if (!shutdownRequest.get()) { - handler.postDelayed(this, 800 /* ms */) - } else { - shutdownTask?.let { - it() - } - } - } - - fun stopTracking() { - shutdownRequest.set(true) - } - - fun stopTracking(callable: () -> Unit) { - shutdownTask = callable - stopTracking() - } + smPlayer?.stop() } } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt index c8822a1c..ddd47a8c 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt @@ -10,13 +10,13 @@ import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import br.com.suamusica.player.PlayerPlugin.Companion.DISABLE_REPEAT_MODE -import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE +import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.ID_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.IS_PLAYING_ARGUMENT -import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY +import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.NEW_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST @@ -25,6 +25,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN import br.com.suamusica.player.PlayerPlugin.Companion.REORDER import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.SEEK_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.SET_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE @@ -66,26 +67,35 @@ class MediaSessionConnection( } } - fun enqueue(medias: String, autoPlay: Boolean,shouldNotifyTransition:Boolean) { + fun enqueue(medias: String, autoPlay: Boolean,) { val bundle = Bundle() bundle.putString("json", medias) bundle.putBoolean("autoPlay", autoPlay) - bundle.putBoolean("shouldNotifyTransition", shouldNotifyTransition) - sendCommand(ENQUEUE, bundle) + sendCommand(ENQUEUE_METHOD, bundle) } fun playFromQueue(index: Int, timePosition: Long, loadOnly: Boolean) { val bundle = Bundle() bundle.putInt(POSITION_ARGUMENT, index) bundle.putLong(TIME_POSITION_ARGUMENT, timePosition) - bundle.putBoolean(LOAD_ONLY, loadOnly) + bundle.putBoolean(LOAD_ONLY_ARGUMENT, loadOnly) sendCommand(PLAY_FROM_QUEUE_METHOD, bundle) } - fun play(shouldPrepare: Boolean = false) { + fun play() { + sendCommand("play", null) + } + + fun cast(id: String) { + val bundle = Bundle() + bundle.putString("cast_id", id) + sendCommand("cast", bundle) + } + + fun setCastMedia(media: String) { val bundle = Bundle() - bundle.putBoolean("shouldPrepare", shouldPrepare) - sendCommand("play", bundle) + bundle.putString("media", media) + sendCommand("cast_next_media", bundle) } fun setRepeatMode(mode: String) { @@ -121,16 +131,17 @@ class MediaSessionConnection( bundle.putInt(ID_FAVORITE_ARGUMENT, idFavorite) sendCommand(UPDATE_FAVORITE, bundle) } + fun updatePlayState(isPlaying: Boolean) { val bundle = Bundle() bundle.putBoolean(IS_PLAYING_ARGUMENT, isPlaying) sendCommand(UPDATE_IS_PLAYING, bundle) } - fun updateMediaUri(id:Int,newUri:String?){ + fun updateMediaUri(id: Int, newUri: String?) { val bundle = Bundle() - bundle.putString(NEW_URI_ARGUMENT,newUri) - bundle.putInt(ID_URI_ARGUMENT,id) + bundle.putString(NEW_URI_ARGUMENT, newUri) + bundle.putInt(ID_URI_ARGUMENT, id) sendCommand(UPDATE_MEDIA_URI, bundle) } @@ -183,7 +194,7 @@ class MediaSessionConnection( val bundle = Bundle() bundle.putLong("position", position) bundle.putBoolean("playWhenReady", playWhenReady) - sendCommand("seek", bundle) + sendCommand(SEEK_METHOD, bundle) } fun release() { diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt index cdda53c1..0945ed87 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt @@ -1,7 +1,5 @@ package br.com.suamusica.player -import com.google.gson.Gson - class MethodChannelManagerArgsBuilder { private val args = mutableMapOf() diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PackageValidator.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PackageValidator.kt index c9b8fa7f..781815a1 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PackageValidator.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PackageValidator.kt @@ -13,7 +13,7 @@ import android.util.Base64 import android.util.Log import androidx.annotation.XmlRes import androidx.media.MediaBrowserServiceCompat -import io.flutter.BuildConfig +import com.google.firebase.encoders.json.BuildConfig import org.xmlpull.v1.XmlPullParserException import java.io.IOException import java.security.MessageDigest @@ -153,20 +153,20 @@ class PackageValidator(context: Context, @XmlRes xmlResId: Int) { private fun buildCallerInfo(callingPackage: String): CallerPackageInfo? { val packageInfo = getPackageInfo(callingPackage) ?: return null - val appName = packageInfo.applicationInfo.loadLabel(packageManager).toString() - val uid = packageInfo.applicationInfo.uid + val appName = packageInfo.applicationInfo?.loadLabel(packageManager).toString() + val uid = packageInfo.applicationInfo?.uid val signature = getSignature(packageInfo) val requestedPermissions = packageInfo.requestedPermissions val permissionFlags = packageInfo.requestedPermissionsFlags val activePermissions = mutableSetOf() requestedPermissions?.forEachIndexed { index, permission -> - if (permissionFlags[index] and PackageInfo.REQUESTED_PERMISSION_GRANTED != 0) { + if (permissionFlags!![index] and PackageInfo.REQUESTED_PERMISSION_GRANTED != 0) { activePermissions += permission } } - return CallerPackageInfo(appName, callingPackage, uid, signature, activePermissions.toSet()) + return CallerPackageInfo(appName, callingPackage, uid!!, signature, activePermissions.toSet()) } /** @@ -193,10 +193,10 @@ class PackageValidator(context: Context, @XmlRes xmlResId: Int) { private fun getSignature(packageInfo: PackageInfo): String? { // Security best practices dictate that an app should be signed with exactly one (1) // signature. Because of this, if there are multiple signatures, reject it. - if (packageInfo.signatures == null || packageInfo.signatures.size != 1) { + if (packageInfo.signatures == null || packageInfo.signatures!!.size != 1) { return null } else { - val certificate = packageInfo.signatures[0].toByteArray() + val certificate = packageInfo.signatures!![0].toByteArray() return getSignatureSha256(certificate) } } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index b05d715d..16c302a4 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -1,20 +1,22 @@ package br.com.suamusica.player - -import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import androidx.media3.common.Player -import androidx.media3.common.Player.* +import androidx.media3.common.Player.STATE_IDLE +import androidx.media3.common.Player.STATE_BUFFERING +import androidx.media3.common.Player.STATE_ENDED +import androidx.media3.common.Player.STATE_READY + class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { - fun notifyStateChange(state: @State Int) { + fun notifyStateChange(state: @Player.State Int) { val playerState = when (state) { - STATE_IDLE, STATE_READY -> PlayerState.IDLE + STATE_IDLE -> PlayerState.IDLE STATE_BUFFERING -> PlayerState.BUFFERING STATE_ENDED -> PlayerState.COMPLETED STATE_READY -> PlayerState.STATE_READY else -> PlayerState.IDLE } - Log.i("Player", "#NATIVE LOGS ==> Notifying Player State change: $playerState | $state") + Log.i("Player", "#NATIVE LOGS Notify ==> Notifying Player State change: $playerState | $state") channelManager.notifyPlayerStateChange("sua-musica-player", playerState) } @@ -23,7 +25,7 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { } fun notifyError(message: String? = null){ - Log.i("Player", "Notifying Error: $message") + Log.i("Player", "#NATIVE LOGS Notify ==> Notifying Error: $message") channelManager.notifyError("sua-musica-player", PlayerState.ERROR, message) } @@ -32,32 +34,23 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { channelManager.notifyPlayerStateChange("sua-musica-player", PlayerState.SEEK_END) } - fun notifyNext() { - Log.i("Player", "Notifying Player Next") - channelManager.notifyNext("sua-musica-player") - } - fun notifyPrevious() { - Log.i("Player", "Notifying Player Previous") - channelManager.notifyPrevious("sua-musica-player") - } fun notifyItemTransition(from:String) { - Log.i("Player", "#NATIVE LOGS ==> notifyItemTransition | FROM: $from") + Log.i("Player", "#NATIVE LOGS Notify ==> notifyItemTransition | FROM: $from") channelManager.notifyItemTransition("sua-musica-player") } fun currentMediaIndex(currentMediaIndex: Int, from: String) { - Log.i("Player", "#NATIVE LOGS ==> currentMediaIndex | FROM: $from | $currentMediaIndex") + Log.i("Player", "#NATIVE LOGS Notify ==> currentMediaIndex | FROM: $from | $currentMediaIndex") channelManager.currentMediaIndex("sua-musica-player", currentMediaIndex) } fun notifyPositionChange(position: Long, duration: Long) { - Log.i("Player", "Notifying Player Position change: position: $position duration: $duration") channelManager.notifyPositionChange("sua-musica-player", position, duration) } fun onRepeatChanged(repeatMode: Int) { - Log.i("Player", "Notifying Player onRepeatChanged: $repeatMode") + Log.i("Player", "#NATIVE LOGS Notify ==> onRepeatChanged: $repeatMode") channelManager.onRepeatChanged("sua-musica-player", repeatMode) } fun onShuffleModeEnabled(shuffleModeEnabled: Boolean) { - Log.i("Player", "Notifying Player onRepeatChanged: $shuffleModeEnabled") + Log.i("Player", "#NATIVE LOGS Notify ==> onRepeatChanged: $shuffleModeEnabled") channelManager.onShuffleModeEnabled("sua-musica-player", shuffleModeEnabled) } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerExtensions.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerExtensions.kt new file mode 100644 index 00000000..ce97c5e7 --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerExtensions.kt @@ -0,0 +1,22 @@ +package br.com.suamusica.player + +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import java.util.WeakHashMap + +val mediaItemMediaAssociations = WeakHashMap() + + +fun Player.getAllMediaItems(): List { + val items = mutableListOf() + for (i in 0 until mediaItemCount) { + getMediaItemAt(i).let { items.add(it) } + } + return items +} + +var MediaItem.associatedMedia: Media? + get() = mediaItemMediaAssociations[this] + set(value) { + mediaItemMediaAssociations[this] = value + } \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt index 24cb0d5b..3a320040 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt @@ -1,5 +1,6 @@ package br.com.suamusica.player +import android.content.pm.ApplicationInfo import android.util.Log import com.google.gson.Gson import com.google.gson.reflect.TypeToken @@ -14,15 +15,13 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { companion object { + private const val CHANNEL = "suamusica.com.br/player" // Argument names - const val NAME_ARGUMENT = "name" - const val AUTHOR_ARGUMENT = "author" - const val URL_ARGUMENT = "url" - const val COVER_URL_ARGUMENT = "coverUrl" - const val BIG_COVER_URL_ARGUMENT = "bigCoverUrl" const val IS_PLAYING_ARGUMENT = "isPlaying" + const val SHOULD_NOTIFY_TRANSITION_ARGUMENT = "shouldNotifyTransition" + const val PLAY_WHEN_READY_ARGUMENT = "playWhenReady" const val IS_FAVORITE_ARGUMENT = "isFavorite" - const val FALLBACK_URL = "fallbackURL" + const val FALLBACK_URL_ARGUMENT = "fallbackURL" const val ID_FAVORITE_ARGUMENT = "idFavorite" const val NEW_URI_ARGUMENT = "newUri" const val ID_URI_ARGUMENT = "idUri" @@ -30,15 +29,15 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val TIME_POSITION_ARGUMENT = "timePosition" const val INDEXES_TO_REMOVE = "indexesToDelete" const val POSITIONS_LIST = "positionsList" - const val LOAD_ONLY = "loadOnly" + const val LOAD_ONLY_ARGUMENT = "loadOnly" const val RELEASE_MODE_ARGUMENT = "releaseMode" - private const val CHANNEL = "suamusica.com.br/player" + const val FAVORITE: String = "favorite" // Method names const val PLAY_METHOD = "play" const val SET_REPEAT_MODE = "set_repeat_mode" - const val ENQUEUE = "enqueue" + const val ENQUEUE_METHOD = "enqueue" const val REMOVE_ALL = "remove_all" const val REMOVE_IN = "remove_in" const val REORDER = "reorder" @@ -50,7 +49,6 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val TOGGLE_SHUFFLE = "toggle_shuffle" const val REPEAT_MODE = "repeat_mode" const val DISABLE_REPEAT_MODE = "disable_repeat_mode" - const val UPDATE_NOTIFICATION = "update_notification" const val UPDATE_FAVORITE = "update_favorite" const val UPDATE_IS_PLAYING = "update_is_playing" const val UPDATE_MEDIA_URI = "update_media_uri" @@ -89,10 +87,11 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { override fun onAttachedToActivity(binding: ActivityPluginBinding) { Log.d(TAG, "onAttachedToActivity") -// val isStopped = (binding.activity.applicationInfo.flags and ApplicationInfo.FLAG_STOPPED) == ApplicationInfo.FLAG_STOPPED -// if(!isStopped){ + // val isStopped = + // (binding.activity.applicationInfo.flags and ApplicationInfo.FLAG_STOPPED) == ApplicationInfo.FLAG_STOPPED + // if (!isStopped) { alreadyAttachedToActivity = true -// } + // } } override fun onDetachedFromActivityForConfigChanges() { @@ -118,38 +117,40 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { } private fun handleMethodCall(call: MethodCall, response: MethodChannel.Result) { - if (call.method == ENQUEUE) { + if (call.method == ENQUEUE_METHOD) { val batch: Map = call.arguments()!! cookie = if (batch.containsKey("cookie")) batch["cookie"] as String else cookie - PlayerSingleton.externalPlayback = - if (batch.containsKey("externalplayback")) batch["externalplayback"].toString() == "true" else PlayerSingleton.externalPlayback } else { cookie = call.argument("cookie") ?: cookie - PlayerSingleton.externalPlayback = call.argument("externalplayback") } Log.d( TAG, "method: ${call.method}" ) when (call.method) { - ENQUEUE -> { + ENQUEUE_METHOD -> { val batch: Map = call.arguments() ?: emptyMap() val listMedia: List> = batch["batch"] as List> val autoPlay: Boolean = (batch["autoPlay"] ?: false) as Boolean - val shouldNotifyTransition: Boolean = - (batch["shouldNotifyTransition"] ?: false) as Boolean val json = Gson().toJson(listMedia) PlayerSingleton.mediaSessionConnection?.enqueue( json, autoPlay, - shouldNotifyTransition, ) } - + "cast" -> { + val id = call.argument("castId") ?: "" + PlayerSingleton.mediaSessionConnection?.cast(id) + } + "cast_next_media" -> { + val media: Map = call.arguments() ?: emptyMap() + val json = Gson().toJson(media) + Log.d(TAG,"NEXT MEDIA: $json") + PlayerSingleton.mediaSessionConnection?.setCastMedia(json) + } PLAY_METHOD -> { - val shouldPrepare = call.argument("shouldPrepare") ?: false - PlayerSingleton.mediaSessionConnection?.play(shouldPrepare) + PlayerSingleton.mediaSessionConnection?.play() } SET_REPEAT_MODE -> { @@ -204,20 +205,18 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { PlayerSingleton.mediaSessionConnection?.previous() } - UPDATE_NOTIFICATION -> { + UPDATE_FAVORITE -> { val isFavorite = call.argument(IS_FAVORITE_ARGUMENT) if (isFavorite != null) { val idFavorite = call.argument(ID_FAVORITE_ARGUMENT) ?: 0 PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite, idFavorite) - } else { - PlayerSingleton.mediaSessionConnection?.updatePlayState(call.argument(IS_PLAYING_ARGUMENT) ?: false) } } PLAY_FROM_QUEUE_METHOD -> { val position = call.argument(POSITION_ARGUMENT) ?: 0 val timePosition = call.argument(TIME_POSITION_ARGUMENT) ?: 0 - val loadOnly = call.argument(LOAD_ONLY) ?: false + val loadOnly = call.argument(LOAD_ONLY_ARGUMENT) ?: false PlayerSingleton.mediaSessionConnection?.playFromQueue( position, timePosition.toLong(), @@ -247,7 +246,8 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { SEEK_METHOD -> { val position = call.argument(POSITION_ARGUMENT)!! - PlayerSingleton.mediaSessionConnection?.seek(position, true) + val playWhenReady = call.argument(PLAY_WHEN_READY_ARGUMENT)!! + PlayerSingleton.mediaSessionConnection?.seek(position, playWhenReady) } REMOVE_NOTIFICATION_METHOD -> { diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt index e0d9e51e..92e27d6f 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt @@ -7,10 +7,14 @@ import io.flutter.plugin.common.MethodChannel object PlayerSingleton { var channel: MethodChannel? = null var mediaSessionConnection: MediaSessionConnection? = null - var externalPlayback: Boolean? = false private const val TAG = "Player" var playerChangeNotifier: PlayerChangeNotifier? = null + var shouldNotifyTransition: Boolean = false + + + var shuffledIndices = mutableListOf() + fun setChannel(c: MethodChannel, context: Context) { channel = c playerChangeNotifier = PlayerChangeNotifier(MethodChannelManager(c)) @@ -21,28 +25,19 @@ object PlayerSingleton { } fun play() { - if (externalPlayback!!) { - channel?.invokeMethod("externalPlayback.play", emptyMap()) - } else { + mediaSessionConnection?.play() channel?.invokeMethod("commandCenter.onPlay", emptyMap()) - } } fun togglePlayPause(){ mediaSessionConnection?.togglePlayPause() channel?.invokeMethod("commandCenter.onTogglePlayPause", emptyMap()) } -// fun adsPlaying(){ -// mediaSessionConnection?.adsPlaying() -// } + fun pause() { - if (externalPlayback!!) { - channel?.invokeMethod("externalPlayback.pause", emptyMap()) - } else { mediaSessionConnection?.pause() channel?.invokeMethod("commandCenter.onPause", emptyMap()) - } } fun previous() { @@ -53,6 +48,23 @@ object PlayerSingleton { channel?.invokeMethod("commandCenter.onNext", emptyMap()) } + fun getNextMedia() { + channel?.invokeMethod("cast.nextMedia", emptyMap()) + Log.d(TAG, "#NATIVE LOGS Notify ==> getNextMedia") + } + + fun getPreviousMedia() { + channel?.invokeMethod("cast.previousMedia", emptyMap()) + Log.d(TAG, "#NATIVE LOGS Notify ==> getPreviousMedia") + } + + fun getMediaFromQueue(index: Int) { + val args = mutableMapOf() + args["index"] = index + channel?.invokeMethod("cast.mediaFromQueue", args) + Log.d(TAG, "#NATIVE LOGS Notify ==> getMediaFromQueue | $index") + } + fun stop() { mediaSessionConnection?.stop() } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt index 631d10f6..d1067642 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt @@ -12,4 +12,5 @@ enum class PlayerState { BUFFER_EMPTY, ITEM_TRANSITION, STATE_READY, + STATE_ENDED, } \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSwitcher.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSwitcher.kt new file mode 100644 index 00000000..4103f8d0 --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSwitcher.kt @@ -0,0 +1,331 @@ +import android.R +import android.content.Context +import android.os.Handler +import android.support.v4.media.session.PlaybackStateCompat +import androidx.core.content.res.ResourcesCompat +import androidx.media3.common.C +import androidx.media3.common.ForwardingPlayer +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.ShuffleOrder +import br.com.suamusica.player.getAllMediaItems +import android.util.Log +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player.STATE_ENDED +import br.com.suamusica.player.MediaButtonEventHandler +import br.com.suamusica.player.PlayerSingleton +import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier +import java.util.concurrent.atomic.AtomicBoolean +import android.os.Looper +import androidx.media3.cast.CastPlayer +import com.google.android.gms.cast.framework.media.RemoteMediaClient + + +@UnstableApi +class PlayerSwitcher( + private var currentPlayer: Player, + private var mediaButtonEventHandler: MediaButtonEventHandler, +) : ForwardingPlayer(currentPlayer) { + private var playerEventListener: Player.Listener? = null + private val TAG = "PlayerSwitcher" + private var progressTracker: ProgressTracker? = null + var remoteMediaClient: RemoteMediaClient? = null + private var playerState: PlayerState? = null + + var oldPlayer: Player? = null + + init { + playerEventListener?.let { currentPlayer.removeListener(it) } + setupPlayerListener() + } + + fun setCurrentPlayer(newPlayer: Player, remoteMediaClient: RemoteMediaClient? = null) { + if (this.currentPlayer === newPlayer) { + return + } + oldPlayer = currentPlayer + this.remoteMediaClient = remoteMediaClient + playerState = savePlayerState() + playerEventListener?.let { currentPlayer.removeListener(it) } + stopAndClearCurrentPlayer() + this.currentPlayer = newPlayer + if (currentPlayer is CastPlayer) { + restorePlayerState(playerState!!) + } + setupPlayerListener() + } + + private data class PlayerState( + val playbackPositionMs: Long = C.TIME_UNSET, + val currentItemIndex: Int = C.INDEX_UNSET, + val playWhenReady: Boolean = false, + val mediaItems: List = emptyList(), + ) + + private fun savePlayerState(): PlayerState { + return PlayerState( + playbackPositionMs = if (currentPlayer.playbackState != STATE_ENDED) currentPlayer.currentPosition else C.TIME_UNSET, + currentItemIndex = currentPlayer.currentMediaItemIndex, + playWhenReady = currentPlayer.playWhenReady, + mediaItems = currentPlayer.getAllMediaItems() + ) + } + + private fun stopAndClearCurrentPlayer() { + currentPlayer.stop() + if (currentPlayer is CastPlayer) { + currentPlayer.clearMediaItems() + } + } + + private fun restorePlayerState(state: PlayerState) { + currentPlayer.setMediaItems( + state.mediaItems, + state.currentItemIndex, + state.playbackPositionMs + ) + currentPlayer.playWhenReady = state.playWhenReady + currentPlayer.prepare() + } + + private fun setupPlayerListener() { + playerEventListener = object : Player.Listener { + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + if (reason == DISCONTINUITY_REASON_SEEK) { + playerChangeNotifier?.notifySeekEnd() + } + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + if (lastState != STATE_BUFFERING) { + playerChangeNotifier?.notifyPlaying(isPlaying) + } + if (isPlaying) { + startTrackingProgress() + } else { + stopTrackingProgress() + } + } + + override fun onMediaItemTransition( + mediaItem: MediaItem?, + reason: @Player.MediaItemTransitionReason Int + ) { + super.onMediaItemTransition(mediaItem, reason) +// oldTransition(mediaItem, reason) + newTransition(reason) + } + + fun newTransition( + reason: @Player.MediaItemTransitionReason Int + ) { + val shouldNotify = + reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == MEDIA_ITEM_TRANSITION_REASON_AUTO + + Log.d( + TAG, + "#NATIVE LOGS ==> onMediaItemTransition reason: $reason | shouldNotNotify: $shouldNotify | shouldNotifyTransition ${PlayerSingleton.shouldNotifyTransition}" + ) + + if (currentPlayer is CastPlayer && reason == MEDIA_ITEM_TRANSITION_REASON_AUTO) { + PlayerSingleton.getNextMedia() + } + + playerChangeNotifier?.currentMediaIndex( + currentIndex(), + "onMediaItemTransition", + ) + + //We not notify when playFromQueue is loadOnly + if (!PlayerSingleton.shouldNotifyTransition) { + return + } + + + mediaButtonEventHandler.buildIcons() + + playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: $reason | shouldNotifyTransition: ${PlayerSingleton.shouldNotifyTransition}") + + PlayerSingleton.shouldNotifyTransition = true + } + + var lastState = PlaybackStateCompat.STATE_NONE - 1 + + override fun onPlaybackStateChanged(playbackState: @Player.State Int) { + super.onPlaybackStateChanged(playbackState) + if (lastState != playbackState) { + lastState = playbackState + playerChangeNotifier?.notifyStateChange(playbackState) + } + + if (playbackState == STATE_ENDED) { + stopTrackingProgressAndPerformTask {} + } + + if (playbackState == STATE_READY && currentPlayer is CastPlayer) { + currentPlayer.repeatMode = REPEAT_MODE_ALL + playerChangeNotifier?.onRepeatChanged(currentPlayer.repeatMode) + } + + Log.d(TAG, "##onPlaybackStateChanged $playbackState") + } + + override fun onPlayerErrorChanged(error: PlaybackException?) { + super.onPlayerErrorChanged(error) + Log.d(TAG, "##onPlayerErrorChanged ${error}") + + } + + override fun onPlayerError(error: PlaybackException) { + Log.d( + "#NATIVE LOGS ==>", + "onPlayerError cause ${error.cause.toString()}" + ) + + playerChangeNotifier?.notifyError( + if (error.cause.toString() + .contains("Permission denied") + ) "Permission denied" else error.message + ) + } + + override fun onRepeatModeChanged(repeatMode: @Player.RepeatMode Int) { + super.onRepeatModeChanged(repeatMode) + playerChangeNotifier?.onRepeatChanged(repeatMode) + } + } + playerEventListener?.let { currentPlayer.addListener(it) } + } + + private fun startTrackingProgress() { + progressTracker?.stopTracking() + progressTracker = ProgressTracker(Handler(Looper.getMainLooper()), this).apply { + setOnPositionChangeListener { position, duration -> + notifyPositionChange() + } + startTracking() + } + } + + fun currentIndex(): Int { + if ((currentPlayer.mediaItemCount) > 0) { + val position = if (currentPlayer.shuffleModeEnabled) { + PlayerSingleton.shuffledIndices.indexOf( + currentPlayer.currentMediaItemIndex + ) + } else { + currentPlayer.currentMediaItemIndex + } + return position + } + return -1 + } + + private fun stopTrackingProgress() { + progressTracker?.stopTracking() + progressTracker = null + } + + + fun setShuffleOrder(shuffleOrder: ShuffleOrder) { + if (currentPlayer is ExoPlayer) { + (currentPlayer as ExoPlayer).setShuffleOrder(shuffleOrder) + } + } + + fun addMediaSource(index: Int, mediaSource: MediaSource) { + if (currentPlayer is ExoPlayer) { + (currentPlayer as ExoPlayer).addMediaSource(index, mediaSource) + } + } + + fun addMediaSources(mediaSources: MutableList) { + if (currentPlayer is ExoPlayer) { + (currentPlayer as ExoPlayer).addMediaSources(mediaSources) + } + } + + + override fun getWrappedPlayer(): Player = currentPlayer + + private fun stopTrackingProgressAndPerformTask(callable: () -> Unit) { + progressTracker?.stopTracking { + callable() + } + progressTracker = null + } + + private fun notifyPositionChange() { + val position = currentPlayer.currentPosition.coerceAtMost(currentPlayer.duration ?: 0L) + val duration = currentPlayer.duration + playerChangeNotifier?.notifyPositionChange(position, duration) + } +} + + +@UnstableApi +class ProgressTracker( + private val handler: Handler, + private val player: PlayerSwitcher, + private val updateIntervalMs: Long = 500 // Intervalo configurável +) : Runnable { + private val TAG = "ProgressTracker" + private val isTracking = AtomicBoolean(false) + private var shutdownTask: (() -> Unit)? = null + private var onPositionChange: ((Long, Long) -> Unit)? = null + + fun startTracking() { + if (isTracking.compareAndSet(false, true)) { + handler.post(this) + } + } + + override fun run() { + if (!isTracking.get()) return + + try { + val position = player.wrappedPlayer.currentPosition + val duration = player.wrappedPlayer.duration + + // Verifica se está próximo do fim + if (duration > 0 && position >= duration - END_THRESHOLD_MS) { + playerChangeNotifier?.notifyStateChange(STATE_ENDED) + Log.d(TAG, "Track completing: position=$position, duration=$duration") + } + + // Notifica mudança de posição + onPositionChange?.invoke(position, duration) + + // Agenda próxima atualização + if (isTracking.get()) { + handler.postDelayed(this, updateIntervalMs) + } + } catch (e: Exception) { + Log.e(TAG, "Error during progress tracking", e) + stopTracking() + } + } + + fun stopTracking(onStopped: (() -> Unit)? = null) { + if (isTracking.compareAndSet(true, false)) { + handler.removeCallbacks(this) + onStopped?.invoke() + } + } + + fun setOnPositionChangeListener(listener: ((Long, Long) -> Unit)?) { + onPositionChange = listener + } + + companion object { + private const val END_THRESHOLD_MS = 800L + } +} \ No newline at end of file diff --git a/packages/player/example/.flutter-plugins-dependencies b/packages/player/example/.flutter-plugins-dependencies index ca9c0226..b14d2245 100644 --- a/packages/player/example/.flutter-plugins-dependencies +++ b/packages/player/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"android":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_android-2.2.10/","native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"macos":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"isar_flutter_libs","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider"]}],"date_created":"2024-09-14 19:45:21.353811","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":true,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"mdns_plugin","path":"/Users/lucastonussi/.pub-cache/git/flutter-6ce9697a9c0a20c7224fd75562164bfb91e1e372/mdns_plugin/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs","file_picker","mdns_plugin"]}],"android":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_plugin_android_lifecycle","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.23/","native_build":true,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"mdns_plugin","path":"/Users/lucastonussi/.pub-cache/git/flutter-6ce9697a9c0a20c7224fd75562164bfb91e1e372/mdns_plugin/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_android-2.2.10/","native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs","file_picker","mdns_plugin"]}],"macos":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]}],"web":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","dependencies":[]}]},"dependencyGraph":[{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"isar_flutter_libs","dependencies":[]},{"name":"mdns_plugin","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider","file_picker","mdns_plugin"]}],"date_created":"2025-01-23 10:06:21.123741","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/packages/player/example/android/app/build.gradle b/packages/player/example/android/app/build.gradle index 9d68c793..4becbe9e 100644 --- a/packages/player/example/android/app/build.gradle +++ b/packages/player/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 34 + compileSdkVersion 35 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/packages/player/example/lib/main.dart b/packages/player/example/lib/main.dart index 102d9078..d00817da 100644 --- a/packages/player/example/lib/main.dart +++ b/packages/player/example/lib/main.dart @@ -9,21 +9,11 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Player example app'), - ), - body: Material( - child: SMPlayer(), - ), + home: Material( + child: SMPlayer(), ), ); } diff --git a/packages/player/example/lib/service_discovery.dart b/packages/player/example/lib/service_discovery.dart new file mode 100644 index 00000000..6cc816d6 --- /dev/null +++ b/packages/player/example/lib/service_discovery.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +import 'package:mdns_plugin/mdns_plugin.dart'; + +class ServiceDiscovery { + ServiceDiscovery(Function(MDNSService)? onFound) { + _flutterMdnsPlugin = MDNSPlugin( + DelegateMDNS( + (MDNSService service) { + final fn = toUTF8String(service.txt?['fn']); + if (fn != null) { + service.map['name'] = fn; + } + onFound?.call(service); + }, + ), + ); + } + late MDNSPlugin _flutterMdnsPlugin; + void startDiscovery() => _flutterMdnsPlugin.startDiscovery( + '_googlecast._tcp', + enableUpdating: true, + ); + + void stopDiscovery() => _flutterMdnsPlugin.stopDiscovery(); +} + +String? toUTF8String(List? bytes) => + bytes == null ? null : const Utf8Codec().decode(bytes); + +class DelegateMDNS implements MDNSPluginDelegate { + DelegateMDNS(this.resolved); + final Function(MDNSService)? resolved; + @override + void onDiscoveryStarted() {} + @override + void onDiscoveryStopped() {} + @override + void onServiceUpdated(MDNSService service) {} + @override + void onServiceRemoved(MDNSService service) {} + @override + bool onServiceFound(MDNSService service) => true; + @override + void onServiceResolved(MDNSService service) { + resolved?.call(service); + } +} diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 25328700..65d84fac 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -4,10 +4,12 @@ import 'package:smplayer/player.dart'; import 'dart:async'; import 'package:flutter/services.dart'; import 'package:smplayer_example/app_colors.dart'; +import 'package:smplayer_example/service_discovery.dart'; import 'package:smplayer_example/ui_data.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:mdns_plugin/mdns_plugin.dart'; class SMPlayer extends StatefulWidget { SMPlayer({ @@ -39,14 +41,14 @@ var media1 = Media( ); var media3 = Media( - id: 1, + id: 3, albumTitle: "Album unsigned", albumId: 1, name: "Track unsigned", url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", coverUrl: "https://picsum.photos/500/500", bigCoverUrl: "https://picsum.photos/500/500", - author: "Xand Avião", + author: "Unknown", fallbackUrl: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", isLocal: false, isVerified: true, @@ -57,7 +59,7 @@ var media3 = Media( ); var media4 = Media( - id: 1, + id: 4, albumTitle: "É o Grelo", albumId: 1, name: "01 - VIDA LOK4 part.1", @@ -99,10 +101,11 @@ var media2 = Media( class _SMPlayerState extends State { late Player _player; Media? currentMedia; - String mediaLabel = ''; Duration duration = Duration(seconds: 0); Duration position = Duration(seconds: 0); var _shuffled = false; + bool _loading = false; + int _currentIndex = 0; @override void initState() { @@ -120,10 +123,14 @@ class _SMPlayerState extends State { initializeIsar: false, ); player.onEvent.listen((Event event) async { - // print( - // "Event: [${event.type}] [${event.media.author}-${event.media.name}] [${event.position}] [${event.duration}]"); - + print("Event: ${event.type}"); switch (event.type) { + case EventType.IDLE: + setState(() { + position = Duration(seconds: 0); + duration = Duration(seconds: 0); + }); + break; case EventType.BEFORE_PLAY: if (event is BeforePlayEvent) { // event.continueWithLoadingOnly(); @@ -137,32 +144,33 @@ class _SMPlayerState extends State { setState(() { position = event.position; duration = event.duration; - currentMedia = event.media; - mediaLabel = toMediaLabel(); }); } } break; - case EventType.PLAYING: + + case EventType.STATE_READY: setState(() { - currentMedia = event.media; - mediaLabel = toMediaLabel(); + _loading = false; }); break; + case EventType.PLAYING: + break; + case EventType.SET_CURRENT_MEDIA_INDEX: + setState(() { + _currentIndex = event.queuePosition; + }); + break; case EventType.PAUSED: + break; + case EventType.BUFFERING: setState(() { - currentMedia = event.media; - mediaLabel = toMediaLabel(); + _loading = true; }); break; - case EventType.NEXT: case EventType.PREVIOUS: - setState(() { - currentMedia = event.media; - mediaLabel = toMediaLabel(); - }); break; default: } @@ -174,7 +182,7 @@ class _SMPlayerState extends State { listOfMedias.addAll([media1, media2, media3, media4]); // } - player.enqueueAll(listOfMedias, autoPlay: false); + // player.enqueueAll(listOfMedias, autoPlay: false); if (!mounted) return; @@ -214,14 +222,7 @@ class _SMPlayerState extends State { void playOrPause() async { print("Player State: ${_player.state}"); - if (_player.state == PlayerState.IDLE && _player.currentMedia != null) { - int result = await _player.play(); - if (result == Player.Ok) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Audio is now playing!!!!'))); - } - } else if (_player.state == PlayerState.BUFFERING && - _player.currentMedia != null) { + if (_player.state == PlayerState.STATE_READY) { int result = await _player.play(); if (result == Player.Ok) { ScaffoldMessenger.of(context) @@ -239,22 +240,18 @@ class _SMPlayerState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Audio is now playing again!!!!'))); } - } else { - int? result = await _player.next(); - if (result == Player.Ok) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Audio is now playing again!!!!'))); - } } } void seek(double p) { - setState(() { - position = Duration(milliseconds: p.round()); - if (_player.state != PlayerState.STOPPED) { - _player.seek(position); - } - }); + setState( + () { + position = Duration(milliseconds: p.round()); + if (_player.state != PlayerState.STOPPED) { + _player.seek(position); + } + }, + ); } void next() { @@ -327,211 +324,295 @@ class _SMPlayerState extends State { } } + Map toTXTMap(Map? txt) { + final map = {}; + txt?.forEach((key, value) { + if (key != null && value != null) { + map.putIfAbsent(key, () => value); + } + }); + + return map; + } + @override Widget build(BuildContext context) { final List colorCodes = [600, 500, 100]; - - return Container( - padding: EdgeInsets.only(top: 8.0, bottom: 0.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.delete), - onPressed: () { - _player.removeAll(); - }, - tooltip: 'Remove all', + List foundServices = []; + + return Scaffold( + drawer: Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: AppColors.primary, ), - IconButton( - icon: Icon(Icons.add), - onPressed: () { - _player.enqueueAll( - [media1, media2, media3, media4], - autoPlay: true, - ); - }, - tooltip: 'Add media', + child: Text( + 'Media Controls', + style: TextStyle( + color: Colors.white, + fontSize: 24, + ), ), - IconButton( - icon: Icon(Icons.queue), - onPressed: () { + ), + ListTile( + leading: Icon(Icons.delete), + title: Text('Remove all'), + onTap: () { + _player.stop(); + _player.removeAll(); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.queue), + title: Text('Add all medias (AutoPlay)'), + onTap: () { + setState(() { _player.enqueueAll( [media1, media2, media3, media4], + autoPlay: true, ); + }); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.queue), + title: Text('Add all medias'), + onTap: () { + _player.enqueueAll( + [media1, media2, media3, media4], + ); + }, + ), + ListTile( + leading: Icon(Icons.folder), + title: Text('Add local file'), + onTap: () { + pickLocalFile(); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.play_arrow), + title: Text('Play from queue (second - 50 seconds)'), + onTap: () { + _player.playFromQueue( + 1, + loadOnly: true, + position: Duration(seconds: 50), + ); + }, + ), + ListTile( + leading: Icon(Icons.add), + title: Text('Add media1'), + onTap: () { + _player.enqueueAll([media1]); + }, + ), + ], + ), + ), + appBar: AppBar( + title: Text('SM Player'), + actions: [ + IconButton( + icon: Icon(Icons.cast), + onPressed: () { + ServiceDiscovery( + (service) { + String castId = + String.fromCharCodes(service.map['txt']['id']); + _player.cast(castId); }, - tooltip: 'Add media', - ), - IconButton( - icon: Icon(Icons.folder), - onPressed: pickLocalFile, - tooltip: 'Add local file', - ), - IconButton( - icon: Icon(Icons.play_arrow), - onPressed: () => _player.playFromQueue(1, - loadOnly: true, position: Duration(seconds: 50)), - tooltip: 'Play from queue', - ), - ], + ).startDiscovery(); + }, ), - Stack( - children: [ - Wrap( - direction: Axis.horizontal, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: EdgeInsets.only(left: 20.0), - child: Text(positionText, - style: TextStyle(fontSize: 14.0)), - ), - Padding( - padding: EdgeInsets.only(right: 20.0), - child: Text(durationText, - style: TextStyle(fontSize: 14.0)), - ) - ], - ), - ], - ), - Container( - width: double.infinity, - margin: EdgeInsets.only(top: 5.0), - child: SliderTheme( - data: SliderTheme.of(context).copyWith( - trackHeight: 2.0, - thumbShape: - const RoundSliderThumbShape(enabledThumbRadius: 7.0), - showValueIndicator: ShowValueIndicator.always, - ), - child: Slider( - activeColor: AppColors.redPink, - inactiveColor: AppColors.inactiveColor, - min: 0.0, - max: duration.inMilliseconds.toDouble(), - value: position.inMilliseconds.toDouble(), - onChanged: (double value) { - seek(value); - }, - ), - ), - ), - ], - ), - Row( - children: [ - Container( - margin: EdgeInsets.only(left: 8), - child: Material( - borderRadius: BorderRadius.circular(25.0), - clipBehavior: Clip.hardEdge, - child: IconButton( - iconSize: 25, - icon: SvgPicture.asset(UIData.btPlayerSuffle, - color: _shuffled - ? AppColors.darkPink - : AppColors.primary), - onPressed: shuffleOrUnshuffle, - )), - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - margin: EdgeInsets.only(left: 8, right: 8), - child: Material( - borderRadius: BorderRadius.circular(40.0), - clipBehavior: Clip.hardEdge, - child: IconButton( - onPressed: previous, - iconSize: 40, - icon: Container( - child: SvgPicture.asset(UIData.btPlayerPrevious), + ], + ), + body: Container( + padding: EdgeInsets.only(top: 8.0, bottom: 0.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Stack( + children: [ + Wrap( + direction: Axis.horizontal, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: EdgeInsets.only(left: 20.0), + child: Text(positionText, + style: TextStyle(fontSize: 14.0)), ), - ), + Padding( + padding: EdgeInsets.only(right: 20.0), + child: Text(durationText, + style: TextStyle(fontSize: 14.0)), + ) + ], + ), + ], + ), + Container( + width: double.infinity, + margin: EdgeInsets.only(top: 5.0), + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 2.0, + thumbShape: + const RoundSliderThumbShape(enabledThumbRadius: 7.0), + showValueIndicator: ShowValueIndicator.always, + ), + child: Slider( + activeColor: AppColors.redPink, + inactiveColor: AppColors.inactiveColor, + min: 0.0, + max: duration.inMilliseconds.toDouble(), + value: position.inMilliseconds.toDouble(), + onChanged: (double value) { + seek(value); + }, ), ), - Material( - borderRadius: BorderRadius.circular(58.0), + ), + ], + ), + Row( + children: [ + Container( + margin: EdgeInsets.only(left: 8), + child: Material( + borderRadius: BorderRadius.circular(25.0), clipBehavior: Clip.hardEdge, child: IconButton( - iconSize: 58, - icon: _player.state == PlayerState.PLAYING - ? SvgPicture.asset(UIData.btPlayerPause) - : SvgPicture.asset(UIData.btPlayerPlay), - onPressed: playOrPause, + iconSize: 25, + icon: SvgPicture.asset(UIData.btPlayerSuffle, + color: _shuffled + ? AppColors.darkPink + : AppColors.primary), + onPressed: shuffleOrUnshuffle, )), - Container( - margin: EdgeInsets.only(left: 8, right: 8), - child: Material( - borderRadius: BorderRadius.circular(40.0), - clipBehavior: Clip.hardEdge, - child: IconButton( + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only(left: 8, right: 8), + child: Material( + borderRadius: BorderRadius.circular(40.0), + clipBehavior: Clip.hardEdge, + child: IconButton( + onPressed: previous, + iconSize: 40, + icon: Container( + child: SvgPicture.asset(UIData.btPlayerPrevious), + ), + ), + ), + ), + Material( + borderRadius: BorderRadius.circular(58.0), + clipBehavior: Clip.hardEdge, + child: IconButton( + iconSize: 58, + icon: _loading + ? Container( + width: 58, + height: 58, + child: CircularProgressIndicator( + strokeWidth: 5, + ), + ) + : _player.state == PlayerState.PLAYING + ? SvgPicture.asset(UIData.btPlayerPause) + : SvgPicture.asset(UIData.btPlayerPlay), + onPressed: playOrPause, + )), + Container( + margin: EdgeInsets.only(left: 8, right: 8), + child: Material( + borderRadius: BorderRadius.circular(40.0), + clipBehavior: Clip.hardEdge, + child: IconButton( onPressed: next, iconSize: 40, icon: Container( child: SvgPicture.asset(UIData.btPlayerNext), - ))), - ), - ], - )), - Container( - margin: EdgeInsets.only(right: 8), - child: Material( - borderRadius: BorderRadius.circular(25.0), - clipBehavior: Clip.hardEdge, - child: IconButton( - iconSize: 25, - icon: SvgPicture.asset(UIData.btPlayerRepeat, - color: _repeatModeToColor()), - onPressed: _player.toggleRepeatMode, - )), - ), - ], - ), - SizedBox(height: 30), - Text(mediaLabel), - SizedBox(height: 30), - Expanded( - child: SizedBox( - height: 200, - child: ReorderableListView( - onReorder: (int oldIndex, int newIndex) { - if (newIndex > _player.items.length) { - newIndex = _player.items.length; - } - if (oldIndex < newIndex) { - newIndex--; - } - _player.reorder(oldIndex, newIndex); - }, - children: _player.items - .mapIndexed( - (index, media) => GestureDetector( - key: Key('queueItemWidgetKey$index'), - child: Container( - height: 50, - color: Colors.blue[colorCodes[index % 3]], - child: Center( - child: Text('${media.id} - ${media.name}')), + ), + ), ), - onTap: () { - _player.playFromQueue(index); - }, ), - ) - .toList(), - ), + ], + ), + ), + Container( + margin: EdgeInsets.only(right: 8), + child: Material( + borderRadius: BorderRadius.circular(25.0), + clipBehavior: Clip.hardEdge, + child: IconButton( + iconSize: 25, + icon: SvgPicture.asset(UIData.btPlayerRepeat, + color: _repeatModeToColor()), + onPressed: _player.toggleRepeatMode, + )), + ), + ], ), - ) - ], + SizedBox(height: 30), + if (_player.items.isNotEmpty) + Text('Tocando: ${_player.items[_currentIndex].name}'), + SizedBox(height: 30), + Expanded( + child: SizedBox( + height: 200, + child: ReorderableListView( + onReorder: (int oldIndex, int newIndex) { + if (newIndex > _player.items.length) { + newIndex = _player.items.length; + } + if (oldIndex < newIndex) { + newIndex--; + } + _player.reorder(oldIndex, newIndex); + }, + children: _player.items + .mapIndexed( + (index, media) => GestureDetector( + key: Key('queueItemWidgetKey$index'), + child: Container( + height: 50, + decoration: BoxDecoration( + border: index == _currentIndex + ? Border.all(color: Colors.red, width: 2.0) + : null, + color: HSLColor.fromAHSL( + 0.8, (index * 137.5) % 360, 0.7, 0.8) + .toColor(), + ), + child: Center( + child: Text('${media.id} - ${media.name}')), + ), + onTap: () { + _player.playFromQueue(index); + }, + ), + ) + .toList(), + ), + ), + ) + ], + ), ), ); } diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index c9ea0c29..20b2dd0e 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -28,10 +28,9 @@ public class PlayerPlugin: NSObject, FlutterPlugin { let listMedia = batch["batch"] as? [[String: Any]] { let autoPlay = batch["autoPlay"] as? Bool ?? false let cookie = batch["cookie"] as? String ?? "" - let shouldNotifyTransition = batch["shouldNotifyTransition"] as? Bool ?? false if let mediaList = convertToMedia(mediaArray: listMedia) { MessageBuffer.shared.send(mediaList) - smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie, shouldNotifyTransition: shouldNotifyTransition) + smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie) } } result(NSNumber(value: true)) @@ -150,6 +149,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { _ = AudioSessionManager.inactivateSession() smPlayer?.clearNowPlayingInfo() smPlayer?.removeAll() + NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil) } } diff --git a/packages/player/ios/Classes/PlayerState.swift b/packages/player/ios/Classes/PlayerState.swift index 9bf7c195..5fe97534 100644 --- a/packages/player/ios/Classes/PlayerState.swift +++ b/packages/player/ios/Classes/PlayerState.swift @@ -18,6 +18,6 @@ enum PlayerState: Int { case seekEnd case bufferEmpty case itemTransition - case stateEnded case stateReady + case stateEnded } diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 7827fa55..adf5f423 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -17,9 +17,8 @@ public class SMPlayer : NSObject { private var isShuffleModeEnabled: Bool = false var shuffledQueue: [AVPlayerItem] = [] private var listeners: SMPlayerListeners? = nil - private var seekToLoadOnly: Bool = false // Transition Control - private var shouldNotifyTransition: Bool = false + private var shouldNotifyTransition: Bool = true var areNotificationCommandsEnabled: Bool = true var fullQueue: [AVPlayerItem] { @@ -38,7 +37,13 @@ public class SMPlayer : NSObject { super.init() self.methodChannelManager = methodChannelManager listeners = SMPlayerListeners(smPlayer:smPlayer,methodChannelManager:methodChannelManager) - listeners?.addPlayerObservers() + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleInterruption(_:)), + name: AVAudioSession.interruptionNotification, + object: nil + ) listeners?.onMediaChanged = { [self] in if(self.smPlayer.items().count > 0){ @@ -47,17 +52,34 @@ public class SMPlayer : NSObject { } shouldNotifyTransition = true self.updateEndPlaybackObserver() - seekToLoadOnly = !seekToLoadOnly self.listeners?.addItemsObservers() - if(seekToLoadOnly){ - seekToLoadOnly = false methodChannelManager?.currentMediaIndex(index: self.currentIndex) - } } } setupNowPlayingInfoCenter() _ = AudioSessionManager.activeSession() } + + @objc private func handleInterruption(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { + return + } + switch type { + case .began: + pause() + case .ended: + if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { + let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) + if options.contains(.shouldResume) { + play() + } + } + @unknown default: + break + } + } func pause() { smPlayer.pause() @@ -111,6 +133,7 @@ public class SMPlayer : NSObject { smPlayer.pause() smPlayer.replaceCurrentItem(with: nil) clearNowPlayingInfo() + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.idle) } func clearNowPlayingInfo() { @@ -118,10 +141,9 @@ public class SMPlayer : NSObject { removeNotification() } - func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String, shouldNotifyTransition: Bool) { + func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { var playerItem: AVPlayerItem? guard let message = MessageBuffer.shared.receive() else { return } - self.shouldNotifyTransition = shouldNotifyTransition if(!cookie.isEmpty){ self.cookie = cookie } @@ -144,11 +166,8 @@ public class SMPlayer : NSObject { self.setNowPlaying() self.enableCommands() } - print("#ENQUEUE: shouldNotifyTransition: \(shouldNotifyTransition)") - if(shouldNotifyTransition){ - methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) - } self.enableCommands() + listeners?.addPlayerObservers() } func removeByPosition(indexes: [Int]) { @@ -355,11 +374,6 @@ public class SMPlayer : NSObject { } func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false) { - if (loadOnly) { - seekToLoadOnly = true - listeners?.mediaChange?.invalidate() - } - listeners?.removeItemObservers() distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position, completionHandler: { print("#NATIVE LOGS ==> completionHandler") self.methodChannelManager?.currentMediaIndex(index: self.currentIndex) @@ -369,6 +383,7 @@ public class SMPlayer : NSObject { }) if(loadOnly){ pause() + shouldNotifyTransition = false }else{ play() } diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index e399816b..398968af 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -22,22 +22,33 @@ public class SMPlayerListeners : NSObject { private var notPlayingReason: NSKeyValueObservation? private var playback: NSKeyValueObservation? + private var lastState = PlayerState.idle + func addItemsObservers() { removeItemObservers() guard let currentItem = smPlayer.currentItem else { return } statusChange = currentItem.observe(\.status, options: [.new, .old]) { (playerItem, change) in - if playerItem.status == .failed { + switch playerItem.status { + case .failed: if let error = playerItem.error { + print("#NATIVE LOGS ==> ERROR: \(String(describing: playerItem.error))") self.methodChannelManager?.notifyError(error: "UNKNOW ERROR") } + case .readyToPlay: + self.notifyPlayerStateChange(state: PlayerState.stateReady) + case .unknown: + break + @unknown default: + break } } + loading = currentItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old]) { [weak self] (new, old) in guard let self = self else { return } print("#NATIVE LOGS ==> Listeners - observer - loading") - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + notifyPlayerStateChange(state: PlayerState.buffering) } loaded = currentItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { (player, _) in @@ -45,6 +56,13 @@ public class SMPlayerListeners : NSObject { } } + func notifyPlayerStateChange(state: PlayerState){ + if(lastState != state){ + self.methodChannelManager?.notifyPlayerStateChange(state: state) + lastState = state + } + } + func addMediaChangeObserver(){ mediaChange = smPlayer.observe(\.currentItem, options: [.new, .old]) { [weak self] (player, change) in @@ -91,13 +109,13 @@ public class SMPlayerListeners : NSObject { guard let self = self else { return } switch player.timeControlStatus { case .playing: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.playing) + notifyPlayerStateChange(state: PlayerState.playing) print("#NATIVE LOGS ==> Listeners - Playing") case .paused: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.paused) + notifyPlayerStateChange(state: PlayerState.paused) print("#NATIVE LOGS ==> Listeners - Paused") case .waitingToPlayAtSpecifiedRate: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + notifyPlayerStateChange(state: PlayerState.buffering) print("#NATIVE LOGS ==> Listeners - Buffering") @unknown default: break @@ -132,7 +150,6 @@ public class SMPlayerListeners : NSObject { notPlayingReason?.invalidate() playback?.invalidate() mediaChange?.invalidate() - mediaChange = nil notPlayingReason = nil playback = nil diff --git a/packages/player/lib/src/event_type.dart b/packages/player/lib/src/event_type.dart index ef272629..28716e4e 100644 --- a/packages/player/lib/src/event_type.dart +++ b/packages/player/lib/src/event_type.dart @@ -39,6 +39,7 @@ enum EventType { STATE_ENDED, IDLE, STATE_READY, + CAST_NEXT_MEDIA, } enum PlayerErrorType { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index e8738172..cb8e0b81 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -31,7 +31,6 @@ class Player { await enqueueAll( items, alreadyAddedToStorage: true, - shouldNotifyTransition: false, ); }, ); @@ -131,12 +130,16 @@ class Player { return Ok; } + Future cast(String castId) async { + await _channel.invokeMethod('cast', {'castId': castId}); + return Ok; + } + Future enqueueAll( List items, { bool autoPlay = false, bool saveOnTop = false, bool alreadyAddedToStorage = false, - bool shouldNotifyTransition = true, }) async { if (!alreadyAddedToStorage) { _queue.addAll(items, saveOnTop: saveOnTop); @@ -172,8 +175,6 @@ class Player { 'playerId': playerId, 'shallSendEvents': _shallSendEvents, 'externalplayback': externalPlayback, - 'shouldNotifyTransition': - batch.length > 1 ? false : shouldNotifyTransition, if (i == 0) ...{ 'cookie': cookie, }, @@ -265,13 +266,8 @@ class Player { int get currentIndex => _queue.index; - Future play({bool shouldPrepare = false}) async { - await _invokeMethod( - 'play', - { - 'shouldPrepare': shouldPrepare, - }, - ); + Future play() async { + await _invokeMethod('play'); return Ok; } @@ -379,11 +375,11 @@ class Player { } } - Future updateNotification({ + Future updateFavorite({ required bool isFavorite, required int id, }) async { - return _channel.invokeMethod('update_notification', { + return _channel.invokeMethod('update_favorite', { 'isFavorite': isFavorite, 'idFavorite': id, }).then((result) => result); @@ -397,16 +393,15 @@ class Player { void addUsingPlayer(Event event) => _addUsingPlayer(player, event); Future stop() async { - // _notifyPlayerStatusChangeEvent(EventType.STOP_REQUESTED); - // final int result = await _invokeMethod('stop'); + _notifyPlayerStatusChangeEvent(EventType.STOP_REQUESTED); + final int result = await _invokeMethod('stop'); - // if (result == Ok) { - // state = PlayerState.STOPPED; - // _notifyPlayerStatusChangeEvent(EventType.STOPPED); - // } + if (result == Ok) { + state = PlayerState.STOPPED; + _notifyPlayerStatusChangeEvent(EventType.STOPPED); + } - // return result; - return Ok; + return result; } Future release() async { @@ -432,9 +427,15 @@ class Player { .invokeMethod('toggle_shuffle', {'positionsList': getPositionsList()}); } - Future seek(Duration position) { + Future seek( + Duration position, { + bool playWhenReady = true, + }) { _notifyPlayerStateChangeEvent(this, EventType.SEEK_START, ""); - return _invokeMethod('seek', {'position': position.inMilliseconds}); + return _invokeMethod('seek', { + 'position': position.inMilliseconds, + 'playWhenReady': playWhenReady, + }); } Future setVolume(double volume) { @@ -457,10 +458,15 @@ class Player { } } + static Future _handleOnComplete(Player player) async { + player.state = PlayerState.COMPLETED; + _notifyPlayerStateChangeEvent(player, EventType.FINISHED_PLAYING, ""); + } + static Future _doHandlePlatformCall(MethodCall call) async { final currentMedia = _queue.current; final currentIndex = _queue.index; - print('call.arguments: ${call.arguments}'); + // print('call.arguments: ${call.arguments}'); final Map callArgs = call.arguments as Map; if (call.method != 'audio.onCurrentPosition') { _log('_platformCallHandler call ${call.method} $callArgs'); @@ -497,6 +503,12 @@ class Player { player.state = PlayerState.values[state]; switch (player.state) { case PlayerState.STATE_READY: + _notifyPlayerStateChangeEvent( + player, + EventType.STATE_READY, + error, + ); + break; case PlayerState.IDLE: _notifyPlayerStateChangeEvent( player, @@ -558,7 +570,7 @@ class Player { break; case PlayerState.COMPLETED: - // _handleOnComplete(player); + _handleOnComplete(player); break; case PlayerState.STATE_ENDED: @@ -668,15 +680,35 @@ class Player { favorite ? EventType.FAVORITE_MUSIC : EventType.UNFAVORITE_MUSIC, "", ); - + break; + case 'cast.mediaFromQueue': + final index = callArgs['index']; + _channel.invokeMethod('cast_next_media', player.items[index].toJson()); + _updateQueueIndexAndNotify( + player: player, + index: index, + ); + break; + case 'cast.nextMedia': + case 'cast.previousMedia': + final media = call.method == 'cast.nextMedia' + ? _queue.possibleNext(_repeatMode) + : _queue.possiblePrevious(); + if (media != null) { + _channel.invokeMethod( + 'cast_next_media', + media.toJson(), + ); + _updateQueueIndexAndNotify( + player: player, + index: player.items.indexOf(media), + ); + } break; case 'SET_CURRENT_MEDIA_INDEX': - _queue.setIndex = callArgs['CURRENT_MEDIA_INDEX']; - _queue.updateIsarIndex(currentMedia!.id, _queue.index); - _notifyPlayerStateChangeEvent( - player, - EventType.SET_CURRENT_MEDIA_INDEX, - "", + _updateQueueIndexAndNotify( + player: player, + index: callArgs['CURRENT_MEDIA_INDEX'], ); break; case 'REPEAT_CHANGED': @@ -838,7 +870,7 @@ class Player { static void _addUsingPlayer(Player player, Event event) { if (event.type != EventType.POSITION_CHANGE) { - debugPrint("_platformCallHandler _addUsingPlayer $event"); + debugPrint("_platformCallHandler _addUsingPlayer ${event.type}"); } if (!player._eventStreamController.isClosed && (player._shallSendEvents || @@ -854,4 +886,17 @@ class Player { } await Future.wait(futures); } + + static void _updateQueueIndexAndNotify({ + required Player player, + required index, + }) { + _queue.setIndex = index; + _queue.updateIsarIndex(player.currentMedia!.id, _queue.index); + _notifyPlayerStateChangeEvent( + player, + EventType.SET_CURRENT_MEDIA_INDEX, + "", + ); + } } diff --git a/packages/player/lib/src/player_state.dart b/packages/player/lib/src/player_state.dart index c3d51628..68750733 100644 --- a/packages/player/lib/src/player_state.dart +++ b/packages/player/lib/src/player_state.dart @@ -9,6 +9,6 @@ enum PlayerState { SEEK_END, BUFFER_EMPTY, ITEM_TRANSITION, - STATE_ENDED, STATE_READY, + STATE_ENDED, } diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index 0e4adb61..13167e90 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -50,10 +50,14 @@ class Queue { Media? _current; set setIndex(int index) { - if (storage.isNotEmpty && index >= 0 && index <= storage.length - 1) { - _index = index; - _current = storage[index].item; + if (storage.isEmpty || index < 0 || index >= storage.length) { + _index = -1; + _current = null; + return; } + + _index = index; + _current = storage[index].item; } final Shuffler _shuffler; diff --git a/packages/player/pubspec.yaml b/packages/player/pubspec.yaml index adf86269..387666c4 100644 --- a/packages/player/pubspec.yaml +++ b/packages/player/pubspec.yaml @@ -24,6 +24,11 @@ dependencies: git: url: https://github.com/SuaMusica/flutter_plugins.git path: packages/aws/ + mdns_plugin: + git: + url: https://github.com/SuaMusica/flutter + path: mdns_plugin + ref: develop dev_dependencies: build_runner: ^2.2.1