diff --git a/packages/player/.gitignore b/packages/player/.gitignore index a8a202ba..2292831d 100644 --- a/packages/player/.gitignore +++ b/packages/player/.gitignore @@ -6,7 +6,9 @@ build/ - +example/linux/ +example/macos/ +example/windows/ # Created by https://www.gitignore.io/api/dart,android,flutter,jetbrains,visualstudio,androidstudio,visualstudiocode # Edit at https://www.gitignore.io/?templates=dart,android,flutter,jetbrains,visualstudio,androidstudio,visualstudiocode diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index 7505b818..96ffe242 100644 --- a/packages/player/android/build.gradle +++ b/packages/player/android/build.gradle @@ -4,14 +4,14 @@ version '1.0.4' buildscript { ext.kotlin_version = '1.9.20' - ext.exoplayer_version = '2.19.1' + ext.media3_version = '1.4.1' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:8.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -44,20 +44,24 @@ android { lintOptions { disable 'InvalidPackage' } - + namespace = "br.com.suamusica.player" } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' - implementation "com.google.android.exoplayer:exoplayer:$exoplayer_version" - implementation "com.google.android.exoplayer:extension-mediasession:$exoplayer_version" implementation "androidx.media:media:1.7.0" implementation "org.jetbrains.kotlin:kotlin-reflect" + //MEDIA3 DEPENDENCIES + implementation "androidx.media3:media3-exoplayer:$media3_version" + implementation "androidx.media3:media3-exoplayer-hls:$media3_version" + implementation "androidx.media3:media3-session:$media3_version" + implementation "androidx.media3:media3-common:$media3_version" + 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' - // Glide dependencies - implementation "com.github.bumptech.glide:glide:4.12.0" -// implementation files('/Users/alantrope/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar') - kapt "com.github.bumptech.glide:compiler:4.12.0" + implementation "com.google.code.gson:gson:2.10.1" } diff --git a/packages/player/android/gradle/wrapper/gradle-wrapper.properties b/packages/player/android/gradle/wrapper/gradle-wrapper.properties index ffed3a25..fae08049 100644 --- a/packages/player/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/player/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/player/android/src/main/AndroidManifest.xml b/packages/player/android/src/main/AndroidManifest.xml index 0d48337f..9fd545b9 100644 --- a/packages/player/android/src/main/AndroidManifest.xml +++ b/packages/player/android/src/main/AndroidManifest.xml @@ -1,22 +1,19 @@ + xmlns:tools="http://schemas.android.com/tools"> + + + + - - - - - - - + \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/FavoriteModeActionProvider.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/FavoriteModeActionProvider.kt deleted file mode 100644 index 31bfcd74..00000000 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/FavoriteModeActionProvider.kt +++ /dev/null @@ -1,28 +0,0 @@ -package br.com.suamusica.player - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.support.v4.media.session.PlaybackStateCompat -import android.util.Log -import androidx.core.app.NotificationCompat -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -import java.util.* - -class FavoriteModeActionProvider(private val context: Context) : - MediaSessionConnector.CustomActionProvider { - - override fun onCustomAction(player: Player, action: String, extras: Bundle?) { - PlayerSingleton.favorite(action == "Favoritar") - } - - override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? { - if (PlayerSingleton.lastFavorite) { - return PlaybackStateCompat.CustomAction.Builder("Desfavoritar", "Desfavoritar", R.drawable.ic_unfavorite_notification_player,).build() - } - return PlaybackStateCompat.CustomAction.Builder("Favoritar", "Favoritar", R.drawable.ic_favorite_notification_player,).build() - } -} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt index 3b6b2763..13dfbb5c 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt @@ -1,10 +1,27 @@ package br.com.suamusica.player -class Media( - val name: String, - val author: String, - val url: String, - val coverUrl: String, - val bigCoverUrl: String?, - val isFavorite: Boolean? +import com.google.gson.annotations.SerializedName + +data class Media( + @SerializedName("id") val id: Long, + @SerializedName("name") val name: String, + @SerializedName("ownerId") val ownerId: Int, + @SerializedName("albumId") val albumId: Long, + @SerializedName("albumTitle") val albumTitle: String, + @SerializedName("author") val author: String, + @SerializedName("url") val url: String, + @SerializedName("is_local") val isLocal: Boolean, + @SerializedName("cover_url") val coverUrl: String, + @SerializedName("bigCover") val bigCoverUrl: String, + @SerializedName("is_verified") val isVerified: Boolean, + @SerializedName("shared_url") val shareUrl: String, + @SerializedName("playlist_id") val playlistId: Long, + @SerializedName("is_spot") val isSpot: Boolean, + @SerializedName("isFavorite") val isFavorite: Boolean?, + @SerializedName("fallbackUrl") val fallbackUrl: String, + @SerializedName("indexInPlaylist") val indexInPlaylist: Int?, + @SerializedName("catid") val categoryId: Int, + @SerializedName("playlistTitle") val playlistTitle: String, + @SerializedName("playlistCoverUrl") val playlistCoverUrl: String, + @SerializedName("playlistOwnerId") val playlistOwnerId: Int ) \ 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 5c43182e..2a9611f2 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 @@ -1,62 +1,403 @@ package br.com.suamusica.player import android.content.Intent +import android.os.Build +import android.os.Bundle import android.util.Log import android.view.KeyEvent -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.Player.COMMAND_GET_TIMELINE +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.util.UnstableApi +import androidx.media3.session.CommandButton +import androidx.media3.session.LibraryResult +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaSession +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.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.NEW_URI_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD +import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST +import br.com.suamusica.player.PlayerPlugin.Companion.POSITION_ARGUMENT +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.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 com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.google.gson.GsonBuilder +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json -class MediaButtonEventHandler : MediaSessionConnector.MediaButtonEventHandler { +@UnstableApi +class MediaButtonEventHandler( + private val mediaService: MediaService, +) : MediaSession.Callback { + private val BROWSABLE_ROOT = "/" + private val EMPTY_ROOT = "@empty@" + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): MediaSession.ConnectionResult { + Log.d("Player", "onConnect") + val sessionCommands = + MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().apply { + add(SessionCommand("notification_next", Bundle.EMPTY)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + add(SessionCommand("notification_previous", Bundle.EMPTY)) + } + add(SessionCommand("notification_favoritar", Bundle.EMPTY)) + add(SessionCommand("notification_desfavoritar", Bundle.EMPTY)) + add(SessionCommand("seek", session.token.extras)) + add(SessionCommand("pause", Bundle.EMPTY)) + add(SessionCommand("stop", Bundle.EMPTY)) + add(SessionCommand("next", Bundle.EMPTY)) + add(SessionCommand("previous", Bundle.EMPTY)) + add(SessionCommand(UPDATE_FAVORITE, session.token.extras)) + add(SessionCommand(FAVORITE, session.token.extras)) + 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(REMOVE_ALL, Bundle.EMPTY)) + add(SessionCommand(REORDER, session.token.extras)) + add(SessionCommand(REMOVE_IN, session.token.extras)) + add(SessionCommand(SET_REPEAT_MODE, session.token.extras)) + add(SessionCommand("prepare", session.token.extras)) + add(SessionCommand("playFromQueue", session.token.extras)) + add(SessionCommand("play", Bundle.EMPTY)) + add(SessionCommand("remove_notification", Bundle.EMPTY)) + add(SessionCommand("send_notification", session.token.extras)) + add(SessionCommand("ads_playing", Bundle.EMPTY)) + add(SessionCommand("onTogglePlayPause", Bundle.EMPTY)) + add(SessionCommand(UPDATE_MEDIA_URI, session.token.extras)) + add(SessionCommand(UPDATE_IS_PLAYING, session.token.extras)) + }.build() - override fun onMediaButtonEvent(player: Player, intent: Intent): Boolean { - onMediaButtonEventHandler(intent) - return true + val playerCommands = + MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() + .remove(COMMAND_SEEK_TO_PREVIOUS) + .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) + .remove(COMMAND_SEEK_TO_NEXT) + .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) + .add(COMMAND_GET_TIMELINE) + .build() + + return MediaSession.ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands(sessionCommands) + .setAvailablePlayerCommands(playerCommands) + .build() } - fun onMediaButtonEventHandler(intent: Intent?) { + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + Log.d("Player", "#MEDIA3# - onCustomCommand ${customCommand.customAction}") + if (customCommand.customAction == "notification_favoritar" || customCommand.customAction == "notification_desfavoritar") { + val isFavorite = customCommand.customAction == "notification_favoritar" + PlayerSingleton.favorite(isFavorite) + buildIcons() + } - if (intent == null) { - return + if(customCommand.customAction == UPDATE_IS_PLAYING){ + buildIcons() } - if (Intent.ACTION_MEDIA_BUTTON == intent.action) { - mediaButtonHandler(intent) - } else if (intent.hasExtra(FAVORITE)) { - PlayerSingleton.favorite(intent.getBooleanExtra(FAVORITE, false)) + if (customCommand.customAction == "seek") { + mediaService.seek(args.getLong("position"), args.getBoolean("playWhenReady")) + } + if (customCommand.customAction == FAVORITE) { + val isFavorite = args.getBoolean(IS_FAVORITE_ARGUMENT) + val mediaItem = session.player.currentMediaItem!! + updateFavoriteMetadata( + session.player, + session.player.currentMediaItemIndex, + mediaItem, + isFavorite, + ) + buildIcons() + } + if (customCommand.customAction == REMOVE_ALL) { + mediaService.removeAll() + } + if (customCommand.customAction == REMOVE_IN) { + mediaService.removeIn( + args.getIntegerArrayList(INDEXES_TO_REMOVE) ?: emptyList() + ) + } + if (customCommand.customAction == REORDER) { + val oldIndex = args.getInt("oldIndex") + val newIndex = args.getInt("newIndex") + val json = args.getString(POSITIONS_LIST) + val gson = GsonBuilder().create() + val mediaListType = object : TypeToken>?>() {}.type + val positionsList: List> = gson.fromJson(json, mediaListType) + + mediaService.reorder(oldIndex, newIndex, positionsList) + } + if (customCommand.customAction == "onTogglePlayPause") { + mediaService.togglePlayPause() + } + if (customCommand.customAction == TOGGLE_SHUFFLE) { +// val list = args.getSerializable("list",ArrayList>()::class.java) + val json = args.getString(POSITIONS_LIST) + val gson = GsonBuilder().create() + val mediaListType = object : TypeToken>>() {}.type + val positionsList: List> = gson.fromJson(json, mediaListType) + mediaService.toggleShuffle(positionsList) + } + if (customCommand.customAction == REPEAT_MODE) { + mediaService.repeatMode() + } + if (customCommand.customAction == DISABLE_REPEAT_MODE) { + mediaService.disableRepeatMode() + } + if (customCommand.customAction == "stop") { + mediaService.stop() + } + if (customCommand.customAction == "play") { + val shouldPrepare = args.getBoolean("shouldPrepare") + mediaService.play(shouldPrepare) + } + if (customCommand.customAction == SET_REPEAT_MODE) { + val mode = args.getString("mode") + mediaService.setRepeatMode(mode ?:"") + } + if (customCommand.customAction == PLAY_FROM_QUEUE_METHOD) { + mediaService.playFromQueue( + args.getInt(POSITION_ARGUMENT), args.getLong(TIME_POSITION_ARGUMENT), + args.getBoolean( + LOAD_ONLY + ), + ) + } + if (customCommand.customAction == "notification_previous" || customCommand.customAction == "previous") { + 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 (customCommand.customAction == "pause") { + mediaService.pause() + } + + if (customCommand.customAction == UPDATE_MEDIA_URI) { + val newUri = args.getString(NEW_URI_ARGUMENT) + val id = args.getInt(ID_URI_ARGUMENT) + session.player.let { + for (i in 0 until it.mediaItemCount) { + val mediaItem = it.getMediaItemAt(i) + if (mediaItem.mediaId == id.toString()) { + mediaService.updateMediaUri(i, newUri) + break + } + } + } + } + + if (customCommand.customAction == UPDATE_FAVORITE) { + val isFavorite = args.getBoolean(IS_FAVORITE_ARGUMENT) + val id = args.getInt(ID_FAVORITE_ARGUMENT) + session.player.let { + for (i in 0 until it.mediaItemCount) { + val mediaItem = it.getMediaItemAt(i) + if (mediaItem.mediaId == id.toString()) { + updateFavoriteMetadata(it, i, mediaItem, isFavorite) + if (id.toString() == session.player.currentMediaItem?.mediaId) { + buildIcons() + } + break + } + } + } + PlayerSingleton.favorite(isFavorite) +// } + } + if (customCommand.customAction == "ads_playing") { +// mediaService.player?.pause() +// mediaService.adsPlaying() + mediaService.removeNotification() + } + if (customCommand.customAction == ENQUEUE) { + val json = args.getString("json") + val gson = GsonBuilder().create() + val mediaListType = object : TypeToken>() {}.type + val mediaList: List = gson.fromJson(json, mediaListType) + buildIcons() + mediaService.enqueue( + mediaList, + args.getBoolean("autoPlay"), + args.getBoolean("shouldNotifyTransition"), + ) } + return Futures.immediateFuture( + SessionResult(SessionResult.RESULT_SUCCESS) + ) + } + private fun updateFavoriteMetadata( + player: Player, + i: Int, + mediaItem: MediaItem, + isFavorite: Boolean + ) { + player.replaceMediaItem( + i, + mediaItem.buildUpon().setMediaMetadata( + mediaItem.mediaMetadata.buildUpon().setExtras( + Bundle().apply { + putBoolean(IS_FAVORITE_ARGUMENT, isFavorite) + } + ).build() + ).build() + ) } - private fun mediaButtonHandler(intent: Intent) { - val ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) - Log.d("Player", "Key: $ke") + fun buildIcons() { + val isFavorite = + mediaService.player?.currentMediaItem?.mediaMetadata?.extras?.getBoolean( + IS_FAVORITE_ARGUMENT + ) ?: false + + val baseList = mutableListOf( + CommandButton.Builder() + .setDisplayName("Save to favorites") + .setIconResId(if (isFavorite) drawable.media3_icon_heart_filled else drawable.media3_icon_heart_unfilled) + .setSessionCommand( + SessionCommand( + if (isFavorite) "notification_desfavoritar" else "notification_favoritar", + Bundle() + ) + ) + .setEnabled(true) + .build() + ) - if (ke!!.action == KeyEvent.ACTION_UP) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + baseList.add( + CommandButton.Builder() + .setDisplayName("notification_next") + .setIconResId(drawable.media3_icon_next) + .setSessionCommand(SessionCommand("notification_next", Bundle.EMPTY)) + .setEnabled(true) + .build() + ) + baseList.add( + CommandButton.Builder() + .setDisplayName("notification_previous") + .setIconResId(drawable.media3_icon_previous) + .setSessionCommand(SessionCommand("notification_previous", Bundle.EMPTY)) + .setEnabled(true) + .build() + ) + } + return mediaService.mediaSession.setCustomLayout(baseList) + } + + + @UnstableApi + override fun onMediaButtonEvent( + session: MediaSession, + controllerInfo: MediaSession.ControllerInfo, + intent: Intent + ): Boolean { + onMediaButtonEventHandler(intent, session) + return true + } + + @UnstableApi + fun onMediaButtonEventHandler(intent: Intent?, session: MediaSession) { + + if (intent == null) { return } - when (ke.keyCode) { - KeyEvent.KEYCODE_MEDIA_PLAY -> { - PlayerSingleton.play() - } - KeyEvent.KEYCODE_MEDIA_PAUSE -> { - PlayerSingleton.pause() - } - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { - Log.d("Player", "Player: Key Code : PlayPause") - PlayerSingleton.togglePlayPause() - } - KeyEvent.KEYCODE_MEDIA_NEXT -> { - Log.d("Player", "Player: Key Code : Next") - PlayerSingleton.next() + if (Intent.ACTION_MEDIA_BUTTON == intent.action) { + @Suppress("DEPRECATION") val ke = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) + } else { + intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) + } + if (ke == null) { + return } - KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { - Log.d("Player", "Player: Key Code : Previous") - PlayerSingleton.previous() + + if (ke.action == KeyEvent.ACTION_UP) { + return } - KeyEvent.KEYCODE_MEDIA_STOP -> { - PlayerSingleton.stop() + + Log.d("Player", "Key: $ke") + Log.d("Player", "#MEDIA3# - Key: $ke") + when (ke.keyCode) { + KeyEvent.KEYCODE_MEDIA_PLAY -> { + PlayerSingleton.play() + } + + KeyEvent.KEYCODE_MEDIA_PAUSE -> { + PlayerSingleton.pause() + } + + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { + Log.d("Player", "Player: Key Code : PlayPause") + if(session.player.isPlaying){ + PlayerSingleton.pause() + }else{ + PlayerSingleton.play() + } + } + + KeyEvent.KEYCODE_MEDIA_NEXT -> { + Log.d("Player", "Player: Key Code : Next") +// session.player.seekToNext() + PlayerSingleton.next() + } + + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { + Log.d("Player", "Player: Key Code : Previous") +// session.player.seekToPrevious() + PlayerSingleton.previous() + } + + KeyEvent.KEYCODE_MEDIA_STOP -> { + PlayerSingleton.stop() + } } + } else if (intent.hasExtra(FAVORITE)) { + PlayerSingleton.favorite(intent.getBooleanExtra(FAVORITE, false)) } + return } -} \ No newline at end of file +} + diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaControlBroadcastReceiver.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaControlBroadcastReceiver.kt deleted file mode 100644 index 3a95336e..00000000 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaControlBroadcastReceiver.kt +++ /dev/null @@ -1,11 +0,0 @@ -package br.com.suamusica.player - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent - -class MediaControlBroadcastReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - MediaButtonEventHandler().onMediaButtonEventHandler(intent) - } -} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaMetadataCompatExt.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaMetadataCompatExt.kt deleted file mode 100644 index b183dbc9..00000000 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaMetadataCompatExt.kt +++ /dev/null @@ -1,371 +0,0 @@ -package br.com.suamusica.player - -import android.graphics.Bitmap -import android.net.Uri -import android.support.v4.media.MediaBrowserCompat.MediaItem -import android.support.v4.media.MediaDescriptionCompat -import android.support.v4.media.MediaMetadataCompat -import android.support.v4.media.MediaMetadataCompat.* -import android.util.Log -import androidx.collection.ArrayMap - -inline val MediaMetadataCompat.METADATA_KEY_IS_VERIFIED: String - get() = "android.media.metadata.METADATA_KEY_IS_VERIFIED" - -inline val MediaMetadataCompat.METADATA_KEY_SHARE_URL: String - get() = "android.media.metadata.METADATA_KEY_SHARE_URL" - -inline val MediaMetadataCompat.METADATA_KEY_ALBUM_ID: String - get() = "android.media.metadata.METADATA_KEY_ALBUM_ID" - -inline val MediaMetadataCompat.METADATA_KEY_PLAYLIST_ID: String - get() = "android.media.metadata.METADATA_KEY_PLAYLIST_ID" - -inline val MediaMetadataCompat.METADATA_KEY_ARTIST_ID: String - get() = "android.media.metadata.METADATA_KEY_ARTIST_ID" - -inline val MediaMetadataCompat.id get() = getString(METADATA_KEY_MEDIA_ID) ?: "" - -inline val MediaMetadataCompat.title get() = getString(METADATA_KEY_TITLE) ?: "" - -inline val MediaMetadataCompat.artist get() = getString(METADATA_KEY_ARTIST) ?: "" - -inline val MediaMetadataCompat.duration - get() = getLong(MediaMetadataCompat.METADATA_KEY_DURATION) ?: 0 - -inline val MediaMetadataCompat.album get() = getString(METADATA_KEY_ALBUM) ?: "" - -inline val MediaMetadataCompat.author get() = getString(METADATA_KEY_AUTHOR) ?: "" - -inline val MediaMetadataCompat.writer get() = getString(METADATA_KEY_WRITER) ?: "" - -inline val MediaMetadataCompat.composer get() = getString(METADATA_KEY_COMPOSER) ?: "" - -inline val MediaMetadataCompat.compilation - get() = getString(METADATA_KEY_COMPILATION) ?: "" - -inline val MediaMetadataCompat.date get() = getString(METADATA_KEY_DATE) ?: "" - -inline val MediaMetadataCompat.year get() = getString(METADATA_KEY_YEAR) ?: "" - -inline val MediaMetadataCompat.genre get() = getString(METADATA_KEY_GENRE) ?: "" - -inline val MediaMetadataCompat.trackNumber - get() = getLong(METADATA_KEY_TRACK_NUMBER) - -inline val MediaMetadataCompat.trackCount - get() = getLong(METADATA_KEY_NUM_TRACKS) - -inline val MediaMetadataCompat.discNumber - get() = getLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER) - -inline val MediaMetadataCompat.albumArtist - get() = getString(METADATA_KEY_ALBUM_ARTIST) ?: "" - -inline val MediaMetadataCompat.art get(): Bitmap? = getBitmap(MediaMetadataCompat.METADATA_KEY_ART) - -inline val MediaMetadataCompat.artUri - get() = Uri.parse(this.getString(METADATA_KEY_ART_URI) ?: "") - -inline val MediaMetadataCompat.albumArt - get(): Bitmap? = getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART) - -inline val MediaMetadataCompat.albumArtUri - get() = Uri.parse(this.getString(METADATA_KEY_ALBUM_ART_URI) ?: "") - -inline val MediaMetadataCompat.userRating - get() = getLong(MediaMetadataCompat.METADATA_KEY_USER_RATING) - -inline val MediaMetadataCompat.rating get() = getLong(MediaMetadataCompat.METADATA_KEY_RATING) - -inline val MediaMetadataCompat.displayTitle - get() = getString(METADATA_KEY_DISPLAY_TITLE) - -inline val MediaMetadataCompat.displaySubtitle - get() = getString(METADATA_KEY_DISPLAY_SUBTITLE) - -inline val MediaMetadataCompat.displayDescription - get() = getString(METADATA_KEY_DISPLAY_DESCRIPTION) - -inline val MediaMetadataCompat.displayIcon - get() = getBitmap(METADATA_KEY_DISPLAY_ICON) - -inline val MediaMetadataCompat.displayIconUri - get() = Uri.parse(this.getString(METADATA_KEY_DISPLAY_ICON_URI) ?: "") - -inline val MediaMetadataCompat.mediaUri - get() = Uri.parse(this.getString(METADATA_KEY_MEDIA_URI) ?: "") - -inline val MediaMetadataCompat.downloadStatus - get() = getLong(METADATA_KEY_DOWNLOAD_STATUS) - -inline val MediaMetadataCompat.isVerified - get(): Boolean = getString("METADATA_KEY_IS_VERIFIED") ?: "0" == "1" - -inline val MediaMetadataCompat.shareUrl - get() = this.getString("METADATA_KEY_SHARE_URL") - -inline val MediaMetadataCompat.albumId - get() = this.getString("METADATA_KEY_ALBUM_ID") - -inline val MediaMetadataCompat.playlistId - get() = getString("METADATA_KEY_PLAYLIST_ID") - -inline val MediaMetadataCompat.artistId - get() = getString("METADATA_KEY_ARTIST_ID") - -// @MediaItem.Flags -inline val MediaMetadataCompat.flag - get() = this.getLong(METADATA_KEY_UAMP_FLAGS).toInt() - -/** - * Useful extensions for [MediaMetadataCompat.Builder]. - */ - -// These do not have getters, so create a message for the error. -const val NO_GET = "Property does not have a 'get'" - -inline var Builder.id: String - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_MEDIA_ID, value) - } - -inline var Builder.title: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_TITLE, value) - } - -inline var Builder.artist: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_ARTIST, value) - } - -inline var Builder.album: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_ALBUM, value) - } - -inline var Builder.duration: Long - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putLong(MediaMetadataCompat.METADATA_KEY_DURATION, value) - } - -inline var Builder.genre: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_GENRE, value) - } - -inline var Builder.mediaUri: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_MEDIA_URI, value) - } - -inline var Builder.albumArtUri: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_ALBUM_ART_URI, value) - } - -inline var Builder.albumArt: Bitmap? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, value) - } - -inline var Builder.trackNumber: Long - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putLong(METADATA_KEY_TRACK_NUMBER, value) - } - -inline var Builder.trackCount: Long - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putLong(METADATA_KEY_NUM_TRACKS, value) - } - -inline var Builder.displayTitle: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_DISPLAY_TITLE, value) - } - -inline var Builder.displaySubtitle: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_DISPLAY_SUBTITLE, value) - } - -inline var Builder.displayDescription: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_DISPLAY_DESCRIPTION, value) - } - -inline var Builder.displayIconUri: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_DISPLAY_ICON_URI, value) - } - -inline var Builder.downloadStatus: Long - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putLong(METADATA_KEY_DOWNLOAD_STATUS, value) - } - -inline var Builder.compilation: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString(METADATA_KEY_COMPILATION, value) - } - -inline var Builder.isVerified: Boolean? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString("METADATA_KEY_IS_VERIFIED", if (value == true) "1" else "0") - } - -inline var Builder.shareUrl: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString("METADATA_KEY_SHARE_URL", value) - } - -inline var Builder.albumId: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString("METADATA_KEY_ALBUM_ID", value) - } - -inline var Builder.playlistId: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString("METADATA_KEY_PLAYLIST_ID", value) - } - -inline var Builder.artistId: String? - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putString("METADATA_KEY_ARTIST_ID", value) - } - -/** - * Custom property for storing whether a [MediaMetadataCompat] item represents an - * item that is [MediaItem.FLAG_BROWSABLE] or [MediaItem.FLAG_PLAYABLE]. - */ -// @MediaItem.Flags -inline var Builder.flag: Int - @Deprecated(NO_GET, level = DeprecationLevel.ERROR) - get() = throw IllegalAccessException("Cannot get from MediaMetadataCompat.Builder") - set(value) { - putLong(METADATA_KEY_UAMP_FLAGS, value.toLong()) - } - -/** - * Custom property for retrieving a [MediaDescriptionCompat] which also includes - * all of the keys from the [MediaMetadataCompat] object in its extras. - * - * These keys are used by the ExoPlayer MediaSession extension when announcing metadata changes. - */ -inline val MediaMetadataCompat.fullDescription - get() = - description.also { - it.extras?.putAll(bundle) - } - -fun MediaMetadataCompat.toMap(): Map { - val mutableMap = mutableMapOf() - - this.let { extras -> - val keySet = extras.keySet() - val iterator = keySet.iterator() - val metadataTypeLong = 0 - val metadataTypeText = 1 - val metadataTypeBitmap = 2 - val metadataTypeRating = 3 - - val metadataKeyType = ArrayMap() - metadataKeyType[METADATA_KEY_TITLE] = metadataTypeText - metadataKeyType[METADATA_KEY_ARTIST] = metadataTypeText - metadataKeyType[METADATA_KEY_ALBUM] = metadataTypeText - metadataKeyType[METADATA_KEY_AUTHOR] = metadataTypeText - metadataKeyType[METADATA_KEY_WRITER] = metadataTypeText - metadataKeyType[METADATA_KEY_COMPOSER] = metadataTypeText - metadataKeyType[METADATA_KEY_COMPILATION] = metadataTypeText - metadataKeyType[METADATA_KEY_DATE] = metadataTypeText - metadataKeyType[METADATA_KEY_GENRE] = metadataTypeText - metadataKeyType[METADATA_KEY_ALBUM_ARTIST] = metadataTypeText - metadataKeyType[METADATA_KEY_ART_URI] = metadataTypeText - metadataKeyType[METADATA_KEY_ALBUM_ART_URI] = metadataTypeText - metadataKeyType[METADATA_KEY_DISPLAY_TITLE] = metadataTypeText - metadataKeyType[METADATA_KEY_DISPLAY_SUBTITLE] = metadataTypeText - metadataKeyType[METADATA_KEY_DISPLAY_DESCRIPTION] = metadataTypeText - metadataKeyType[METADATA_KEY_DISPLAY_ICON_URI] = metadataTypeText - metadataKeyType[METADATA_KEY_MEDIA_ID] = metadataTypeText - metadataKeyType[METADATA_KEY_MEDIA_URI] = metadataTypeText - metadataKeyType[METADATA_KEY_DURATION] = metadataTypeLong - metadataKeyType[METADATA_KEY_YEAR] = metadataTypeLong - metadataKeyType[METADATA_KEY_TRACK_NUMBER] = metadataTypeLong - metadataKeyType[METADATA_KEY_NUM_TRACKS] = metadataTypeLong - metadataKeyType[METADATA_KEY_DISC_NUMBER] = metadataTypeLong - metadataKeyType[METADATA_KEY_BT_FOLDER_TYPE] = metadataTypeLong - metadataKeyType[METADATA_KEY_ADVERTISEMENT] = metadataTypeLong - metadataKeyType[METADATA_KEY_DOWNLOAD_STATUS] = metadataTypeLong - metadataKeyType[METADATA_KEY_ART] = metadataTypeBitmap - metadataKeyType[METADATA_KEY_ALBUM_ART] = metadataTypeBitmap - metadataKeyType[METADATA_KEY_DISPLAY_ICON] = metadataTypeBitmap - metadataKeyType[METADATA_KEY_USER_RATING] = metadataTypeRating - metadataKeyType[METADATA_KEY_RATING] = metadataTypeRating - - - while (iterator.hasNext()) { - val key = iterator.next() - when (metadataKeyType[key]) { - metadataTypeLong -> mutableMap[key] = extras.getLong(key) - metadataTypeText -> mutableMap[key] = extras.getString(key) - metadataTypeRating -> mutableMap[key] = extras.getRating(key) - metadataTypeBitmap -> Log.d("MediaMetadataCompat", "toMap - ignoring Bitmap.") - else -> Log.d("MediaMetadataCompat", "toMap - ignoring ${metadataKeyType[key]}.") - } - } - } - - return mutableMap.toMap() -} - - -/** - * Custom property that holds whether an item is [MediaItem.FLAG_BROWSABLE] or - * [MediaItem.FLAG_PLAYABLE]. - */ -const val METADATA_KEY_UAMP_FLAGS = "com.example.android.uamp.media.METADATA_KEY_UAMP_FLAGS" \ No newline at end of file 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 07f44808..5d270ba7 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,378 +1,366 @@ package br.com.suamusica.player -import android.annotation.SuppressLint -import android.app.Notification +import android.app.ActivityManager +import android.app.NotificationManager import android.app.PendingIntent import android.app.Service -import android.content.ComponentName -import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.net.Uri -import android.net.wifi.WifiManager import android.os.Build import android.os.Bundle import android.os.Handler -import android.os.PowerManager -import android.support.v4.media.MediaBrowserCompat -import android.support.v4.media.MediaDescriptionCompat -import android.support.v4.media.MediaMetadataCompat -import android.support.v4.media.session.MediaControllerCompat -import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat -import android.util.Log import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat -import androidx.media.session.MediaButtonReceiver +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 +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DataSourceBitmapLoader +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.datasource.FileDataSource +import androidx.media3.exoplayer.ExoPlayer +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.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.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.FutureTarget -import com.bumptech.glide.request.RequestOptions -import com.google.android.exoplayer2.C -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.PlaybackException -import com.google.android.exoplayer2.PlaybackParameters -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK -import com.google.android.exoplayer2.Timeline -import com.google.android.exoplayer2.Tracks -import com.google.android.exoplayer2.audio.AudioAttributes -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator -import com.google.android.exoplayer2.source.ProgressiveMediaSource -import com.google.android.exoplayer2.source.hls.HlsMediaSource -import com.google.android.exoplayer2.upstream.DataSource -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource -import com.google.android.exoplayer2.upstream.FileDataSource -import com.google.android.exoplayer2.util.Util -import kotlinx.coroutines.DelicateCoroutinesApi +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 -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.io.File -import java.io.BufferedReader -import java.io.InputStreamReader -import java.io.IOException -import java.util.concurrent.TimeUnit +import java.util.Collections +import java.util.WeakHashMap import java.util.concurrent.atomic.AtomicBoolean -class MediaService : androidx.media.MediaBrowserServiceCompat() { +const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" +const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 + +@UnstableApi +class MediaService : MediaSessionService() { private val TAG = "MediaService" private val userAgent = "SuaMusica/player (Linux; Android ${Build.VERSION.SDK_INT}; ${Build.BRAND}/${Build.MODEL})" - private var packageValidator: PackageValidator? = null - - private var mediaSession: MediaSessionCompat? = null - private var mediaController: MediaControllerCompat? = null - private var mediaSessionConnector: MediaSessionConnector? = null - private var media: Media? = null - - private var notificationBuilder: NotificationBuilder? = null - private var notificationManager: NotificationManagerCompat? = null private var isForegroundService = false - private var wifiLock: WifiManager.WifiLock? = null - private var wakeLock: PowerManager.WakeLock? = null - private val uAmpAudioAttributes = AudioAttributes.Builder() - .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) - .setUsage(C.USAGE_MEDIA) - .build() + 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 var player: ExoPlayer? = null + var player: ExoPlayer? = null private var progressTracker: ProgressTracker? = null - private var previousState: Int = -1 + private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader + private lateinit var mediaButtonEventHandler: MediaButtonEventHandler + private var shuffleOrder: DefaultShuffleOrder? = null - private val BROWSABLE_ROOT = "/" - private val EMPTY_ROOT = "@empty@" + 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 isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true) - private val isHyperOS = !getProperty("ro.mi.os.version.name").isNullOrBlank() + private val channel = Channel>(Channel.BUFFERED) + private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + private val mediaItemMediaAssociations = WeakHashMap() - companion object { - private val glideOptions = RequestOptions() - .fallback(R.drawable.default_art) - .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) - .timeout(5000) + override fun onCreate() { + super.onCreate() + mediaButtonEventHandler = MediaButtonEventHandler(this) - private const val NOTIFICATION_LARGE_ICON_SIZE = 500 // px - private const val LOCAL_COVER_PNG = "../app_flutter/covers/0.png" // px - @OptIn(DelicateCoroutinesApi::class) - fun getArts(context: Context, artUri: String?, callback: (Bitmap?) -> Unit) { - GlobalScope.launch(Dispatchers.IO) { - Log.i("getArts", " artUri: $artUri") - val glider = Glide.with(context) - .applyDefaultRequestOptions(glideOptions) - .asBitmap() - val file = File(context.filesDir, LOCAL_COVER_PNG) - var bitmap: Bitmap? = null - val futureTarget: FutureTarget? = when { - !artUri.isNullOrBlank() -> glider.load(artUri) - .submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE) + player = ExoPlayer.Builder(this).build().apply { + setAudioAttributes(uAmpAudioAttributes, true) + addListener(playerEventListener()) + setWakeMode(C.WAKE_MODE_NETWORK) + setHandleAudioBecomingNoisy(true) + preloadConfiguration = ExoPlayer.PreloadConfiguration( + 10000000 + ) + } - file.exists() -> glider.load(Uri.fromFile(file)) - .submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE) + dataSourceBitmapLoader = + DataSourceBitmapLoader(applicationContext) + + player?.let { + mediaSession = MediaSession.Builder(this, it) + .setBitmapLoader(CacheBitmapLoader(dataSourceBitmapLoader)) + .setCallback(mediaButtonEventHandler) + .setSessionActivity(getPendingIntent()) + .build() + this@MediaService.setMediaNotificationProvider(object : MediaNotification.Provider { + override fun createNotification( + mediaSession: MediaSession, + customLayout: ImmutableList, + actionFactory: MediaNotification.ActionFactory, + onNotificationChangedCallback: MediaNotification.Provider.Callback + ): MediaNotification { + val defaultMediaNotificationProvider = + DefaultMediaNotificationProvider(applicationContext) + .apply { + setSmallIcon(R.drawable.ic_notification) + } - else -> null - } + val customMedia3Notification = + defaultMediaNotificationProvider.createNotification( + mediaSession, + mediaSession.customLayout, + actionFactory, + onNotificationChangedCallback, + ) - if (futureTarget != null) { - bitmap = try { - futureTarget.get() - } catch (e: Exception) { - Log.i("getArts", "ART EXCP: $e") - if (file.exists()) { - BitmapFactory.decodeFile(file.absolutePath) - } else { - null - } - } + return MediaNotification( + NOW_PLAYING_NOTIFICATION, + customMedia3Notification.notification + ) } - withContext(Dispatchers.Main) { - callback(bitmap) + + override fun handleCustomCommand( + session: MediaSession, + action: String, + extras: Bundle + ): Boolean { + Log.d(TAG, "#MEDIA3# - handleCustomCommand $action") + return false } - } + }) } } + fun removeNotification() { + Log.d("Player", "removeNotification") + player?.stop() +// NotificationManagerCompat.from(applicationContext).cancelAll() + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d(TAG, "onStartCommand") + super.onStartCommand(intent, flags, startId) return Service.START_STICKY - } - override fun onCreate() { - super.onCreate() - packageValidator = PackageValidator(applicationContext, R.xml.allowed_media_browser_callers) - notificationBuilder = NotificationBuilder(this) - notificationManager = NotificationManagerCompat.from(this) - wifiLock = (applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager) - .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "suamusica:wifiLock") - wakeLock = (applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager) - .newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK or PowerManager.ON_AFTER_RELEASE, - "suamusica:wakeLock" - ) - wifiLock?.setReferenceCounted(false) - wakeLock?.setReferenceCounted(false) - - val sessionActivityPendingIntent = - this.packageManager?.getLaunchIntentForPackage(this.packageName)?.let { sessionIntent -> - PendingIntent.getActivity(this, 0, sessionIntent, PendingIntent.FLAG_IMMUTABLE) - } - - val mediaButtonReceiver = ComponentName(this, MediaButtonReceiver::class.java) - mediaSession = mediaSession?.let { it } - ?: MediaSessionCompat(this, TAG, mediaButtonReceiver, null) - .apply { - setSessionActivity(sessionActivityPendingIntent) - isActive = true - } - - mediaSession?.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) - - player = ExoPlayer.Builder(this).build().apply { - setAudioAttributes(uAmpAudioAttributes, true) - addListener(playerEventListener()) - // setWakeMode(C.WAKE_MODE_NETWORK) - setHandleAudioBecomingNoisy(true) - } - mediaSession?.let { mediaSession -> - val sessionToken = mediaSession.sessionToken - // we must connect the service to the media session - this.sessionToken = sessionToken - - val mediaControllerCallback = MediaControllerCallback() - - mediaController = MediaControllerCompat(this, sessionToken).also { mediaController -> - mediaController.registerCallback(mediaControllerCallback) - - mediaSessionConnector = MediaSessionConnector(mediaSession).also { connector -> - connector.setPlayer(player) - connector.setPlaybackPreparer(MusicPlayerPlaybackPreparer(this)) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (isSamsung || isHyperOS) { - connector.setCustomActionProviders( - FavoriteModeActionProvider(applicationContext), - NextActionProvider(), - PreviousActionProvider(), - ) - } else { - connector.setCustomActionProviders( - FavoriteModeActionProvider(applicationContext), - PreviousActionProvider(), - NextActionProvider(), - ) - } - } - connector.setMediaButtonEventHandler(MediaButtonEventHandler()) - connector.setEnabledPlaybackActions( - PlaybackStateCompat.ACTION_PLAY - or PlaybackStateCompat.ACTION_PAUSE - or PlaybackStateCompat.ACTION_REWIND - or PlaybackStateCompat.ACTION_FAST_FORWARD - or PlaybackStateCompat.ACTION_SEEK_TO - ) - } - } + private fun getPendingIntent(): PendingIntent { + val notifyIntent = Intent("SUA_MUSICA_FLUTTER_NOTIFICATION_CLICK").apply { + addCategory(Intent.CATEGORY_DEFAULT) + flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP } + + return PendingIntent.getActivity( + applicationContext, + 0, + notifyIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT else PendingIntent.FLAG_UPDATE_CURRENT + ) } - override fun onTaskRemoved(rootIntent: Intent) { - Log.d(TAG, "onTaskRemoved") - super.onTaskRemoved(rootIntent) + override fun onGetSession( + controllerInfo: MediaSession.ControllerInfo + ): MediaSession = mediaSession - /** - * By stopping playback, the player will transition to [Player.STATE_IDLE]. This will - * cause a state change in the MediaSession, and (most importantly) call - * [MediaControllerCallback.onPlaybackStateChanged]. Because the playback state will - * be reported as [PlaybackStateCompat.STATE_NONE], the service will first remove - * itself as a foreground service, and will then call [stopSelf]. - */ + override fun onTaskRemoved(rootIntent: Intent?) { + player?.clearMediaItems() + Log.d(TAG, "onTaskRemoved") player?.stop() - stopService() + stopTrackingProgress() + stopSelf() + super.onTaskRemoved(rootIntent) } override fun onDestroy() { - removeNotification() - Log.d(TAG, "onDestroy") -// mediaController?.unregisterCallback(mediaControllerCallback) - releaseLock() - mediaSessionConnector?.setPlayer(null) - player?.release() - stopSelf() - - mediaSession?.run { - isActive = false + mediaSession.run { + releaseAndPerformAndDisableTracking() + player.release() release() - Log.d("MusicService", "onDestroy(isActive: $isActive)") + mediaSession.release() } - + consumer.cancel() + channel.cancel() releasePossibleLeaks() + stopSelf() super.onDestroy() - } private fun releasePossibleLeaks() { player?.release() - notificationManager = null - notificationBuilder = null - packageValidator = null - mediaSession = null + mediaSession.release() mediaController = null - mediaSessionConnector = null - wifiLock = null - wakeLock = 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 + } + } + return false } - private fun acquireLock(duration: Long) { - wifiLock?.acquire() - wakeLock?.acquire(duration) + fun updateMediaUri(index: Int, uri: String?) { +// if (index != player?.currentMediaItemIndex) { + val media = player?.getMediaItemAt(index) + media?.associatedMedia?.let { + player?.removeMediaItem(index) + player?.addMediaSource( + index, prepare( + cookie, + it, + uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL) ?: "" + ) + ) +// player?.prepare() + } +// } + } + + fun toggleShuffle(positionsList: List>) { + player?.shuffleModeEnabled = !(player?.shuffleModeEnabled ?: false) + player?.shuffleModeEnabled?.let { + if (it) { + shuffledIndices.clear() + for (e in positionsList) { + shuffledIndices.add(e["originalPosition"] ?: 0) + } + shuffleOrder = DefaultShuffleOrder( + shuffledIndices.toIntArray(), + System.currentTimeMillis() + ) + Log.d( + TAG, + "toggleShuffle - shuffleOrder is null: ${shuffleOrder == null} | shuffledIndices: ${shuffledIndices.size} - ${player?.mediaItemCount}" + ) + player!!.setShuffleOrder(shuffleOrder!!) + } + playerChangeNotifier?.onShuffleModeEnabled(it) + } } - private fun releaseLock() { - try { - if (wifiLock?.isHeld == true) wifiLock?.release() - if (wakeLock?.isHeld == true) wakeLock?.release() - } catch (e: Exception) { - Log.e("MusicService", e.message, e) + private fun addToQueue(item: List) { + serviceScope.launch { + channel.send(item) } } - override fun onGetRoot( - clientPackageName: String, - clientUid: Int, - rootHints: Bundle? - ): BrowserRoot? { - val isKnowCaller = packageValidator?.isKnownCaller(clientPackageName, clientUid) ?: false + private fun processItem(item: List) { + createMediaSource(cookie, item) + } - return if (isKnowCaller) { - BrowserRoot(BROWSABLE_ROOT, null) - } else { - BrowserRoot(EMPTY_ROOT, null) + private val consumer = serviceScope.launch { + channel.receiveAsFlow().collect { item -> + processItem(item) } } - override fun onLoadChildren( - parentId: String, - result: Result> + fun enqueue( + medias: List, + autoPlay: Boolean, + shouldNotifyTransition: Boolean, ) { - result.sendResult(mutableListOf()) + Log.d( + TAG, + "enqueue: mediaItemCount: ${player?.mediaItemCount} | autoPlay: $autoPlay" + ) + this.autoPlay = autoPlay + this.shouldNotifyTransition = shouldNotifyTransition + if (player?.mediaItemCount == 0) { + player?.playWhenReady = autoPlay + } +// enqueueLoadOnly = autoPlay + android.util.Log.d( + "#NATIVE LOGS ==>", + "enqueue $autoPlay | mediaItemCount: ${player?.mediaItemCount} | shouldNotifyTransition: $shouldNotifyTransition" + ) + addToQueue(medias) + } + + private fun createMediaSource(cookie: String, medias: List) { + val mediaSources: MutableList = mutableListOf() + if (medias.isNotEmpty()) { + 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") + } } - fun prepare(cookie: String, media: Media) { - this.media = media - + private fun prepare(cookie: String, media: Media, urlToPrepare: String): MediaSource { val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setReadTimeoutMs(15 * 1000) dataSourceFactory.setConnectTimeoutMs(10 * 1000) dataSourceFactory.setUserAgent(userAgent) dataSourceFactory.setAllowCrossProtocolRedirects(true) dataSourceFactory.setDefaultRequestProperties(mapOf("Cookie" to cookie)) - - // Metadata Build - val metadataBuilder = MediaMetadataCompat.Builder() - val art = null - metadataBuilder.apply { - album = media.author - albumArt = art - title = media.name - displayTitle = media.name - putString(MediaMetadataCompat.METADATA_KEY_ARTIST, media.author) - putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, media.name) - putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art) - putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, art) + 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 metadata = metadataBuilder.build() - mediaSession?.setMetadata(metadata) - mediaSessionConnector?.setMediaMetadataProvider { - return@setMediaMetadataProvider metadata - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - val timelineQueueNavigator = object : TimelineQueueNavigator(mediaSession!!) { - override fun getSupportedQueueNavigatorActions(player: Player): Long { - return PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT or - PlaybackStateCompat.ACTION_SEEK_TO - } + val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata) + .setMediaId(media.id.toString()).build() + mediaItem.associatedMedia = media + @C.ContentType val type = Util.inferContentType(uri) - override fun getMediaDescription( - player: Player, - windowIndex: Int - ): MediaDescriptionCompat { - player.let { - return MediaDescriptionCompat.Builder().apply { - setTitle(media.author) - setSubtitle(media.name) - setIconUri(Uri.parse(media.coverUrl)) - }.build() - } - } + return when (type) { + C.CONTENT_TYPE_HLS -> { + HlsMediaSource.Factory(dataSourceFactory) + .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) + .setAllowChunklessPreparation(true) + .createMediaSource(mediaItem) } - mediaSessionConnector?.setQueueNavigator(timelineQueueNavigator) - } - val url = media.url - Log.i(TAG, "Player: URL: $url") - - val uri = if (url.startsWith("/")) Uri.fromFile(File(url)) else Uri.parse(url) - @C.ContentType val type = Util.inferContentType(uri) - Log.i(TAG, "Player: Type: $type HLS: ${C.TYPE_HLS}") - val source = when (type) { - C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory) - .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) - .setAllowChunklessPreparation(true) - .createMediaSource(MediaItem.fromUri(uri)) - C.TYPE_OTHER -> { - Log.i(TAG, "Player: URI: $uri") + C.CONTENT_TYPE_OTHER -> { val factory: DataSource.Factory = if (uri.scheme != null && uri.scheme?.startsWith("http") == true) { dataSourceFactory @@ -380,72 +368,135 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { FileDataSource.Factory() } - ProgressiveMediaSource.Factory(factory).createMediaSource(MediaItem.fromUri(uri)) + ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem) } + else -> { throw IllegalStateException("Unsupported type: $type") } } - player?.pause() - player?.prepare(source) } - fun play() { - performAndEnableTracking { - player?.play() + fun reorder( + oldIndex: Int, + newIndex: Int, + positionsList: List> + ) { + if (player?.shuffleModeEnabled == true) { + val list = shuffledIndices.ifEmpty { + positionsList.map { it["originalPosition"] ?: 0 }.toMutableList() + } + Collections.swap(list, oldIndex, newIndex) + shuffleOrder = + DefaultShuffleOrder(list.toIntArray(), System.currentTimeMillis()) + player?.setShuffleOrder(shuffleOrder!!) + } else { + player?.moveMediaItem(oldIndex, newIndex) } } - fun adsPlaying() { - getArts(applicationContext,null) { bitmap -> - this.media = Media("Propaganda", "", "", "",null,null ) - val notification = buildNotification(PlaybackStateCompat.STATE_PLAYING, true, bitmap) - notification?.let { - notificationManager?.notify(NOW_PLAYING_NOTIFICATION, it) - shouldStartService(it) + + fun removeIn(indexes: List) { + val sortedIndexes = indexes.sortedDescending() + if (sortedIndexes.isNotEmpty()) { + sortedIndexes.forEach { + player?.removeMediaItem(it) + if (shuffledIndices.isNotEmpty()) { + shuffledIndices.removeAt( + shuffledIndices.indexOf( + player?.currentMediaItemIndex ?: 0 + ) + ) + } } } + if (player?.shuffleModeEnabled == true) { + shuffleOrder = DefaultShuffleOrder( + shuffledIndices.toIntArray(), + System.currentTimeMillis() + ) + player?.setShuffleOrder(shuffleOrder!!) + } } - fun sendCommand(type: String) { - val extra = Bundle() - extra.putString("type", type) - mediaSession?.setExtras(extra) + fun disableRepeatMode() { + player?.repeatMode = REPEAT_MODE_OFF } - fun setFavorite(favorite: Boolean?) { - media?.let { - this.media = Media(it.name, it.author, it.url, it.coverUrl, it.bigCoverUrl, favorite) - sendNotification(this.media!!, null) - } - } + fun repeatMode() { + player?.let { + when (it.repeatMode) { + REPEAT_MODE_OFF -> { + it.repeatMode = REPEAT_MODE_ALL + } - fun sendNotification(media: Media, isPlayingExternal: Boolean?) { - getArts(applicationContext, media.bigCoverUrl ?: media.coverUrl) { bitmap -> - mediaSession?.let { - val onGoing: Boolean = if (isPlayingExternal == null) { - val state = player?.playbackState ?: PlaybackStateCompat.STATE_NONE - state == PlaybackStateCompat.STATE_PLAYING || state == PlaybackStateCompat.STATE_BUFFERING - } else { - isPlayingExternal + REPEAT_MODE_ONE -> { + it.repeatMode = REPEAT_MODE_OFF } - this.media = media - val notification = notificationBuilder?.buildNotification( - it, - media, - onGoing, - isPlayingExternal, - media.isFavorite, - player?.duration, bitmap - ) - notification?.let { - notificationManager?.notify(NOW_PLAYING_NOTIFICATION, notification) + + else -> { + it.repeatMode = REPEAT_MODE_ONE } } } } - fun removeNotification() { - removeNowPlayingNotification(); + private fun buildMetaData(media: Media): MediaMetadata { + val metadataBuilder = MediaMetadata.Builder() + + val bundle = Bundle() + bundle.putBoolean(IS_FAVORITE_ARGUMENT, media.isFavorite ?: false) + bundle.putString(FALLBACK_URL, media.fallbackUrl) + metadataBuilder.apply { + setAlbumTitle(media.name) + setArtist(media.author) + setArtworkUri(Uri.parse(media.bigCoverUrl)) + setArtist(media.author) + setTitle(media.name) + setDisplayTitle(media.name) + setExtras(bundle) + } + val metadata = metadataBuilder.build() + 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, + timePosition, + ) + if (!loadOnly) { + player?.prepare() + playerChangeNotifier?.notifyItemTransition("playFromQueue") + } + } + + fun removeAll() { + player?.stop() + player?.clearMediaItems() } fun seek(position: Long, playWhenReady: Boolean) { @@ -466,42 +517,25 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { } fun togglePlayPause() { - performAndDisableTracking { - if (player?.isPlaying == true) { - player?.pause() - } else { - player?.play() - } + if (player?.isPlaying == true) { + pause() + } else { + play() } } - fun release() { + private fun releaseAndPerformAndDisableTracking() { performAndDisableTracking { player?.stop() } } - private fun removeNowPlayingNotification() { - Log.d(TAG, "removeNowPlayingNotification") - Thread(Runnable { - notificationManager?.cancel(NOW_PLAYING_NOTIFICATION) - }).start() - - } - private fun notifyPositionChange() { var position = player?.currentPosition ?: 0L val duration = player?.duration ?: 0L position = if (position > duration) duration else position - - if (duration > 0) { - val extra = Bundle() - extra.putString("type", "position") - extra.putLong("position", position) - extra.putLong("duration", duration) - mediaSession?.setExtras(extra) - } + playerChangeNotifier?.notifyPositionChange(position, duration) } private fun startTrackingProgress() { @@ -535,139 +569,111 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { stopTrackingProgress() } - private fun buildNotification( - updatedState: Int, - onGoing: Boolean, - art: Bitmap? - ): Notification? { - return if (updatedState != PlaybackStateCompat.STATE_NONE) { - mediaSession?.let { - notificationBuilder?.buildNotification( - it, - media, - onGoing, - null, - media?.isFavorite, - player?.duration, - art - ) - } + fun currentIndex(): Int { + val position = if (player?.shuffleModeEnabled == true) { + shuffledIndices.indexOf( + player?.currentMediaItemIndex ?: 0 + ) } else { - null + player?.currentMediaItemIndex ?: 0 } + return position } private fun playerEventListener(): Player.Listener { return object : Player.Listener { - override fun onTimelineChanged(timeline: Timeline, reason: Int) { - Log.i(TAG, "onTimelineChanged: timeline: $timeline reason: $reason") + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + if (reason == DISCONTINUITY_REASON_SEEK) { + playerChangeNotifier?.notifySeekEnd() + } } - override fun onTracksChanged(tracks: Tracks) { - Log.i(TAG, "onTracksChanged: ") + 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 onLoadingChanged(isLoading: Boolean) { - Log.i(TAG, "onLoadingChanged: isLoading: $isLoading") + 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 } - override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { - Log.i( - TAG, - "onPlayerStateChanged: playWhenReady: $playWhenReady playbackState: $playbackState currentPlaybackState: ${player?.playbackState}" - ) - if (playWhenReady) { - val duration = player?.duration ?: 0L - acquireLock( - if (duration > 1L) duration + TimeUnit.MINUTES.toMillis(2) else TimeUnit.MINUTES.toMillis( - 3 - ) - ) - } else - releaseLock() + var lastState = PlaybackStateCompat.STATE_NONE - 1 - if (playWhenReady && playbackState == ExoPlayer.STATE_READY) { - // - } else { - if (player?.playerError != null) { - // - } else { - when (playbackState) { - ExoPlayer.STATE_IDLE -> { // 1 - // - } - ExoPlayer.STATE_BUFFERING -> { // 2 - // - } - ExoPlayer.STATE_READY -> { // 3 - val status = - if (playWhenReady) PlayerState.PLAYING else PlayerState.PAUSED - if (previousState == -1) { - // when we define that the track shall not "playWhenReady" - // no position info is sent - // therefore, we need to "emulate" the first position notification - // by sending it directly - notifyPositionChange() - } else { - if (status == PlayerState.PAUSED) { - stopTrackingProgressAndPerformTask { - // - } - } else { - // - } - - } - } - ExoPlayer.STATE_ENDED -> { // 4 - stopTrackingProgressAndPerformTask { - // - } - } - } - } + override fun onPlaybackStateChanged(playbackState: @Player.State Int) { + super.onPlaybackStateChanged(playbackState) + if (lastState != playbackState) { + lastState = playbackState + playerChangeNotifier?.notifyStateChange(playbackState) + } + + if (playbackState == STATE_ENDED) { + stopTrackingProgressAndPerformTask {} } - previousState = playbackState + Log.d(TAG, "##onPlaybackStateChanged $playbackState") } - override fun onRepeatModeChanged(repeatMode: Int) { - Log.i(TAG, "onRepeatModeChanged: $repeatMode") + override fun onPlayerErrorChanged(error: PlaybackException?) { + super.onPlayerErrorChanged(error) + Log.d(TAG, "##onPlayerErrorChanged ${error}") + } - override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { - Log.i(TAG, "onShuffleModeEnabledChanged: $shuffleModeEnabled") + override fun onRepeatModeChanged(repeatMode: @Player.RepeatMode Int) { + super.onRepeatModeChanged(repeatMode) + playerChangeNotifier?.onRepeatChanged(repeatMode) } override fun onPlayerError(error: PlaybackException) { - Log.e(TAG, "onPLayerError: ${error.message}", error) - val bundle = Bundle() - bundle.putString("type", "error") - bundle.putString( - "error", + 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 ) - mediaSession?.setExtras(bundle) - } - - override fun onPositionDiscontinuity(reason: Int) { - Log.i(TAG, "onPositionDiscontinuity: $reason") - if (reason == DISCONTINUITY_REASON_SEEK) { - val bundle = Bundle() - bundle.putString("type", "seek-end") - mediaSession?.setExtras(bundle) - } - } override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { - Log.i(TAG, "onPlaybackParametersChanged: $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 @@ -677,6 +683,14 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { } 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()) { @@ -697,115 +711,4 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { stopTracking() } } - - fun shouldStartService(notification: Notification) { - if (!isForegroundService) { - Log.i(TAG, "Starting Service") - try { - ContextCompat.startForegroundService( - applicationContext, - Intent(applicationContext, this@MediaService.javaClass) - ) - startForeground(NOW_PLAYING_NOTIFICATION, notification) - } catch (e: Exception) { - startForeground(NOW_PLAYING_NOTIFICATION, notification) - ContextCompat.startForegroundService( - applicationContext, - Intent(applicationContext, this@MediaService.javaClass) - ) - } - isForegroundService = true - - } - } - - fun stopService() { - if (isForegroundService) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - stopForeground(STOP_FOREGROUND_DETACH) - } else { - stopForeground(false) - } - isForegroundService = false - stopSelf() - Log.i(TAG, "Stopping Service") - } - } - - private inner class MediaControllerCallback : MediaControllerCompat.Callback() { - override fun onMetadataChanged(metadata: MediaMetadataCompat?) { - Log.d( - TAG, - "onMetadataChanged: title: ${metadata?.title} duration: ${metadata?.duration}" - ) - } - - override fun onPlaybackStateChanged(state: PlaybackStateCompat?) { - Log.d(TAG, "onPlaybackStateChanged state: $state") - updateNotification(state!!) - } - - override fun onQueueChanged(queue: MutableList?) { - Log.d(TAG, "onQueueChanged queue: $queue") - } - - @SuppressLint("WakelockTimeout") - private fun updateNotification(state: PlaybackStateCompat) { - if (mediaController?.metadata == null || mediaSession == null) { - return - } - getArts(applicationContext,media?.bigCoverUrl ?: media?.coverUrl) { bitmap -> - val updatedState = state.state - val onGoing = - updatedState == PlaybackStateCompat.STATE_PLAYING || updatedState == PlaybackStateCompat.STATE_BUFFERING - // Skip building a notification when state is "none". - val notification = if (updatedState != PlaybackStateCompat.STATE_NONE) { - buildNotification(updatedState, onGoing, bitmap) - } else { - null - } - Log.d(TAG, "!!! updateNotification state: $updatedState $onGoing") - - when (updatedState) { - PlaybackStateCompat.STATE_BUFFERING, - PlaybackStateCompat.STATE_PLAYING -> { - Log.i(TAG, "updateNotification: STATE_BUFFERING or STATE_PLAYING") - /** - * This may look strange, but the documentation for [Service.startForeground] - * notes that "calling this method does *not* put the service in the started - * state itself, even though the name sounds like it." - */ - if (notification != null) { - notificationManager?.notify(NOW_PLAYING_NOTIFICATION, notification) - shouldStartService(notification) - } - } - else -> { - if (isForegroundService) { - // If playback has ended, also stop the service. - if (updatedState == PlaybackStateCompat.STATE_NONE && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - stopService() - } - if (notification != null) { - notificationManager?.notify(NOW_PLAYING_NOTIFICATION, notification) - } else - removeNowPlayingNotification() - } - } - } - } - } - } - - // to get the property value from build.prop. - private fun getProperty(property: String): String? { - return try { - Runtime.getRuntime().exec("getprop $property").inputStream.use { input -> - BufferedReader(InputStreamReader(input), 1024).readLine() - } - } catch (e: IOException) { - e.printStackTrace() - null - } - } } 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 23e59a97..c8822a1c 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 @@ -9,11 +9,34 @@ import android.support.v4.media.MediaMetadataCompat 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.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.NEW_URI_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD +import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST +import br.com.suamusica.player.PlayerPlugin.Companion.POSITION_ARGUMENT +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.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 com.google.gson.Gson import java.lang.ref.WeakReference class MediaSessionConnection( - context: Context, - val playerChangeNotifier: PlayerChangeNotifier + context: Context, + val playerChangeNotifier: PlayerChangeNotifier ) { val TAG = "Player" @@ -31,7 +54,8 @@ class MediaSessionConnection( var duration = 0L private val weakContext = WeakReference(context) - private val weakServiceComponent = WeakReference(ComponentName(context, MediaService::class.java)) + private val weakServiceComponent = + WeakReference(ComponentName(context, MediaService::class.java)) private val mediaBrowserConnectionCallback = MediaBrowserConnectionCallback(context) private var mediaBrowser: MediaBrowserCompat? = null @@ -42,29 +66,47 @@ class MediaSessionConnection( } } - fun prepare(cookie: String, media: Media) { + fun enqueue(medias: String, autoPlay: Boolean,shouldNotifyTransition:Boolean) { val bundle = Bundle() - bundle.putString("cookie", cookie) - bundle.putString("name", media.name) - bundle.putString("author", media.author) - bundle.putString("url", media.url) - bundle.putString("coverUrl", media.coverUrl) - bundle.putString("bigCoverUrl", media.bigCoverUrl) - - PlayerSingleton.lastFavorite = media.isFavorite ?: false - if (media.isFavorite != null) { - bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, media.isFavorite) - } - sendCommand("prepare", bundle) + bundle.putString("json", medias) + bundle.putBoolean("autoPlay", autoPlay) + bundle.putBoolean("shouldNotifyTransition", shouldNotifyTransition) + sendCommand(ENQUEUE, 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) + sendCommand(PLAY_FROM_QUEUE_METHOD, bundle) } - fun play() { - sendCommand("play", null) + fun play(shouldPrepare: Boolean = false) { + val bundle = Bundle() + bundle.putBoolean("shouldPrepare", shouldPrepare) + sendCommand("play", bundle) + } + + fun setRepeatMode(mode: String) { + val bundle = Bundle() + bundle.putString("mode", mode) + sendCommand(SET_REPEAT_MODE, bundle) + } + + fun reorder(oldIndex: Int, newIndex: Int, positionsList: List>) { + val bundle = Bundle() + bundle.putInt("oldIndex", oldIndex) + bundle.putInt("newIndex", newIndex) + val json = Gson().toJson(positionsList) + bundle.putString(POSITIONS_LIST, json) + sendCommand(REORDER, bundle) } fun togglePlayPause() { - sendCommand("togglePlayPause", null) + sendCommand("onTogglePlayPause", null) } + fun adsPlaying() { sendCommand("ads_playing", null) } @@ -73,10 +115,64 @@ class MediaSessionConnection( sendCommand("pause", null) } - fun favorite(shouldFavorite:Boolean) { + fun updateFavorite(isFavorite: Boolean, idFavorite: Int) { + val bundle = Bundle() + bundle.putBoolean(IS_FAVORITE_ARGUMENT, isFavorite) + 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?){ val bundle = Bundle() - bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, shouldFavorite) - sendCommand(FAVORITE, bundle) + bundle.putString(NEW_URI_ARGUMENT,newUri) + bundle.putInt(ID_URI_ARGUMENT,id) + sendCommand(UPDATE_MEDIA_URI, bundle) + } + + fun removeAll() { + sendCommand(REMOVE_ALL, null) + } + + fun removeIn(indexes: List) { + val bundle = Bundle() + bundle.putIntegerArrayList(INDEXES_TO_REMOVE, ArrayList(indexes)) + sendCommand(REMOVE_IN, bundle) + } + + fun next() { + sendCommand("next", null) + } + + fun toggleShuffle(positionsList: List>) { + val bundle = Bundle() + //API33 +// bundle.putSerializable(POSITIONS_LIST, ArrayList(positionsList)) + val json = Gson().toJson(positionsList) + bundle.putString(POSITIONS_LIST, json) + sendCommand(TOGGLE_SHUFFLE, bundle) + } + + fun repeatMode() { + sendCommand(REPEAT_MODE, null) + } + + fun disableRepeatMode() { + sendCommand(DISABLE_REPEAT_MODE, null) + } + + fun previous() { + sendCommand("previous", null) + } + + fun favorite(shouldFavorite: Boolean) { + val bundle = Bundle() + bundle.putBoolean(IS_FAVORITE_ARGUMENT, shouldFavorite) + sendCommand(PlayerPlugin.FAVORITE, bundle) } fun stop() { @@ -94,28 +190,15 @@ class MediaSessionConnection( sendCommand("release", null) } - fun sendNotification(name: String, author: String, url: String, coverUrl: String, isPlaying: Boolean?, bigCoverUrl: String?, isFavorite: Boolean?) { - val bundle = Bundle() - bundle.putString("name", name) - bundle.putString("author", author) - bundle.putString("url", url) - bundle.putString("coverUrl", coverUrl) - bundle.putString("bigCoverUrl", bigCoverUrl) - if (isPlaying != null) { - bundle.putBoolean(PlayerPlugin.IS_PLAYING_ARGUMENT, isPlaying) - } - PlayerSingleton.lastFavorite = isFavorite ?: false - if (isFavorite != null) { - bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, isFavorite) - } - sendCommand("send_notification", bundle) - } - fun removeNotification() { sendCommand("remove_notification", null) } - private fun sendCommand(command: String, bundle: Bundle? = null, callbackHandler: ResultReceiver? = null) { + private fun sendCommand( + command: String, + bundle: Bundle? = null, + callbackHandler: ResultReceiver? = null + ) { ensureMediaBrowser { ensureMediaController { it.sendCommand(command, bundle, callbackHandler) @@ -128,8 +211,10 @@ class MediaSessionConnection( if (mediaBrowser == null) { val context = weakContext.get() val serviceComponent = weakServiceComponent.get() - mediaBrowser = MediaBrowserCompat(context, serviceComponent, - mediaBrowserConnectionCallback, null) + mediaBrowser = MediaBrowserCompat( + context, serviceComponent, + mediaBrowserConnectionCallback, null + ) } mediaBrowser?.let { @@ -142,7 +227,10 @@ class MediaSessionConnection( } } catch (e: Exception) { if (e.message?.contains("connect() called while neither disconnecting nor disconnected") == true) - Log.i("Player", "MediaBrowser is CONNECT_STATE_CONNECTING(2) or CONNECT_STATE_CONNECTED(3) or CONNECT_STATE_SUSPENDED(4)") + Log.i( + "Player", + "MediaBrowser is CONNECT_STATE_CONNECTING(2) or CONNECT_STATE_CONNECTED(3) or CONNECT_STATE_SUSPENDED(4)" + ) else Log.e("Player", "Failed", e) } @@ -152,8 +240,8 @@ class MediaSessionConnection( mediaController?.let(callable) } - private inner class MediaBrowserConnectionCallback(private val context: Context) - : MediaBrowserCompat.ConnectionCallback() { + private inner class MediaBrowserConnectionCallback(private val context: Context) : + MediaBrowserCompat.ConnectionCallback() { override fun onConnected() { Log.i(TAG, "MediaBrowserConnectionCallback.onConnected : STARTED") mediaBrowser?.let { mediaBrowser -> @@ -161,48 +249,6 @@ class MediaSessionConnection( return mediaController = MediaControllerCompat(context, mediaBrowser.sessionToken) - mediaController?.registerCallback(object : MediaControllerCompat.Callback() { - var lastState = PlaybackStateCompat.STATE_NONE - 1 - override fun onPlaybackStateChanged(state: PlaybackStateCompat) { - if (lastState != state.state) { - Log.i(TAG, "onPlaybackStateChanged: $state") - lastState = state.state - playerChangeNotifier.notifyStateChange(state.state) - } - } - - override fun onExtrasChanged(extras: Bundle) { - if (extras.containsKey("type")) { - when (extras.getString("type")) { - "position" -> { - val position = extras.getLong("position") - this@MediaSessionConnection.currentPosition = position - val duration = extras.getLong("duration") - this@MediaSessionConnection.duration = duration - playerChangeNotifier.notifyPositionChange(position, duration) - } - "error" -> { - val error = extras.getString("error") - playerChangeNotifier.notifyStateChange(PlaybackStateCompat.STATE_ERROR, error) - } - "seek-end" -> { - playerChangeNotifier.notifySeekEnd() - } - "next" -> { - playerChangeNotifier.notifyNext() - } - "previous" -> { - playerChangeNotifier.notifyPrevious() - } - } - } - super.onExtrasChanged(extras) - } - - override fun onMetadataChanged(metadata: MediaMetadataCompat) { - Log.i(TAG, "onMetadataChanged: $metadata duration: ${metadata.duration}") - } - }) } Log.i(TAG, "MediaBrowserConnectionCallback.onConnected : ENDED") } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt index 338c42ae..0c0dfaf2 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt @@ -6,40 +6,76 @@ class MethodChannelManager(private val channel: MethodChannel) { fun notifyPositionChange(playerId: String, position: Long, duration: Long) { val args = ArgsBuilder() - .playerId(playerId) - .position(position) - .duration(duration) - .build() + .playerId(playerId) + .position(position) + .duration(duration) + .build() invokeMethod("audio.onCurrentPosition", args) } - fun notifyPlayerStateChange(playerId: String, state: PlayerState, error: String? = null) { + fun notifyPlayerStateChange(playerId: String, state: PlayerState) { val args = ArgsBuilder() - .playerId(playerId) - .state(state) - .error(error) - .build() + .playerId(playerId) + .state(state) + .build() + + invokeMethod("state.change", args) + } + fun notifyError(playerId: String, state: PlayerState, error: String? = null) { + val args = ArgsBuilder() + .playerId(playerId) + .state(state) + .error(error) + .build() invokeMethod("state.change", args) } fun notifyNext(playerId: String) { val args = ArgsBuilder() - .playerId(playerId) - .build() + .playerId(playerId) + .build() invokeMethod("commandCenter.onNext", args) } fun notifyPrevious(playerId: String) { val args = ArgsBuilder() - .playerId(playerId) - .build() + .playerId(playerId) + .build() invokeMethod("commandCenter.onPrevious", args) } - + fun notifyItemTransition(playerId: String) { + val args = ArgsBuilder() + .playerId(playerId) + .state(PlayerState.ITEM_TRANSITION) + .build() + invokeMethod("state.change", args) + } private fun invokeMethod(method: String, args: Map) { channel.invokeMethod(method, args) } + fun currentMediaIndex(playerId: String, currentMediaIndex: Int) { + val args = MethodChannelManagerArgsBuilder() + .playerId(playerId) + .currentMediaIndex(currentMediaIndex) + .build() + invokeMethod("SET_CURRENT_MEDIA_INDEX", args) + } + fun onRepeatChanged(playerId: String, repeatMode: Int) { + val args = MethodChannelManagerArgsBuilder() + .playerId(playerId) + .repeatMode(repeatMode) + .build() + invokeMethod("REPEAT_CHANGED", args) + } + fun onShuffleModeEnabled(playerId: String, shuffleModeEnabled: Boolean) { + val args = MethodChannelManagerArgsBuilder() + .playerId(playerId) + .shuffleModeEnabled(shuffleModeEnabled) + .build() + invokeMethod("SHUFFLE_CHANGED", args) + } + } \ No newline at end of file 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 new file mode 100644 index 00000000..cdda53c1 --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt @@ -0,0 +1,39 @@ +package br.com.suamusica.player + +import com.google.gson.Gson + +class MethodChannelManagerArgsBuilder { + + private val args = mutableMapOf() + + fun build() = args + + fun event(event: String): MethodChannelManagerArgsBuilder { + args["EVENT_ARGS"] = event + return this + } + fun playerId(id: String): MethodChannelManagerArgsBuilder { + args["playerId"] = id + return this + } + + fun currentMediaIndex(index: Int): MethodChannelManagerArgsBuilder { + args["CURRENT_MEDIA_INDEX"] = index + return this + } + + fun repeatMode(repeatMode: Int): MethodChannelManagerArgsBuilder { + args["REPEAT_MODE"] = repeatMode + return this + } + + fun shuffleModeEnabled(shuffleModeEnabled: Boolean): MethodChannelManagerArgsBuilder { + args["SHUFFLE_MODE"] = shuffleModeEnabled + return this + } + + fun idSum(idSum: Long): MethodChannelManagerArgsBuilder { + args["ID_SUM"] = idSum + return this + } +} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MusicPlayerPlaybackPreparer.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MusicPlayerPlaybackPreparer.kt deleted file mode 100644 index abb82b12..00000000 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MusicPlayerPlaybackPreparer.kt +++ /dev/null @@ -1,133 +0,0 @@ -package br.com.suamusica.player - -import android.net.Uri -import android.os.Bundle -import android.os.ResultReceiver -import android.util.Log -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector - -class MusicPlayerPlaybackPreparer( - private val mediaService: MediaService, - ) : MediaSessionConnector.PlaybackPreparer { - val TAG = "Player" - - override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) { - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromMediaId : START") - - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromMediaId : END") - } - - override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) { - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromSearch : START") - - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromSearch : END") - } - - override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) { - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromUri : START") - - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromUri : END") - } - - override fun onCommand(player: Player, - command: String, extras: Bundle?, cb: ResultReceiver?): Boolean { - try { - Log.i(TAG, "MusicPlayerPlaybackPreparer.onCommand : START") - - return when (command) { - "prepare" -> { - return extras?.let { - val cookie = it.getString("cookie")!! - val name = it.getString("name")!! - val author = it.getString("author")!! - val url = it.getString("url")!! - val coverUrl = it.getString("coverUrl")!! - val bigCoverUrl = it.getString("bigCoverUrl") - - var isFavorite:Boolean? = null; - if(it.containsKey(PlayerPlugin.IS_FAVORITE_ARGUMENT)){ - isFavorite = it.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT) - } - mediaService.prepare(cookie, Media(name, author, url, coverUrl, bigCoverUrl,isFavorite)) - return@let true - } ?: false - } - "play" -> { - mediaService.play() - true - } - "pause" -> { - mediaService.pause() - true - } - "stop" -> { - mediaService.stop() - true - } - "togglePlayPause" -> { - mediaService.togglePlayPause() - true - } - "release" -> { - mediaService.release() - true - } - "seek" -> { - return extras?.let { - val position = it.getLong("position") - val playWhenReady = it.getBoolean("playWhenReady") - mediaService.seek(position, playWhenReady) - return@let true - } ?: false - } - "remove_notification" -> { - mediaService.removeNotification() - return true - } - "send_notification" -> { - return extras?.let { - val name = it.getString("name")!! - val author = it.getString("author")!! - val url = it.getString("url")!! - val coverUrl = it.getString("coverUrl")!! - val bigCoverUrl = it.getString("bigCoverUrl") - var isPlaying:Boolean? = null; - var isFavorite:Boolean? = null; - if(it.containsKey(PlayerPlugin.IS_PLAYING_ARGUMENT)){ - isPlaying = it.getBoolean(PlayerPlugin.IS_PLAYING_ARGUMENT) - } - if(it.containsKey(PlayerPlugin.IS_FAVORITE_ARGUMENT)){ - isFavorite = it.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT) - } - mediaService.sendNotification(Media(name, author, url, coverUrl, bigCoverUrl, isFavorite),isPlaying) - return true - } ?: false - } - "ads_playing" -> { - mediaService.adsPlaying() - return true - } - FAVORITE -> { - return extras?.let { - if(it.containsKey(PlayerPlugin.IS_FAVORITE_ARGUMENT)){ - mediaService.setFavorite(it.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT)) - } - return@let true - } ?: false - } - else -> false - } - } finally { - Log.i(TAG, "MusicPlayerPlaybackPreparer.onCommand : END") - } - } - - override fun getSupportedPrepareActions(): Long { - return 0L - } - - override fun onPrepare(playWhenReady: Boolean) { - - } -} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/NextActionProvider.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/NextActionProvider.kt deleted file mode 100644 index 097b75ca..00000000 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/NextActionProvider.kt +++ /dev/null @@ -1,23 +0,0 @@ -package br.com.suamusica.player - -import android.os.Bundle -import android.support.v4.media.session.PlaybackStateCompat -import android.util.Log -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector - -class NextActionProvider : - MediaSessionConnector.CustomActionProvider { - - override fun onCustomAction(player: Player, action: String, extras: Bundle?) { - PlayerSingleton.next() - } - - override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? { - return PlaybackStateCompat.CustomAction.Builder( - "Ir a próxima música", - "Ir a próxima música", - R.drawable.ic_next_notification_player, - ).build() - } -} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/NotificationBuilder.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/NotificationBuilder.kt deleted file mode 100644 index 9fbb1272..00000000 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/NotificationBuilder.kt +++ /dev/null @@ -1,235 +0,0 @@ -package br.com.suamusica.player - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.media.MediaMetadata -import android.os.Build -import android.support.v4.media.MediaMetadataCompat -import android.support.v4.media.session.MediaSessionCompat -import android.support.v4.media.session.PlaybackStateCompat.* -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.media.app.NotificationCompat.MediaStyle -import androidx.media.session.MediaButtonReceiver -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.RequestOptions -import kotlinx.coroutines.* -import java.util.* - -const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" -const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 -const val FAVORITE: String = "favorite" - -/** - * Helper class to encapsulate code for building notifications. - */ -class NotificationBuilder(private val context: Context) { - private val platformNotificationManager: NotificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - private val skipToPreviousAction = NotificationCompat.Action( - R.drawable.ic_prev_notification_player, - context.getString(R.string.notification_skip_to_previous), - MediaButtonReceiver.buildMediaButtonPendingIntent(context, ACTION_SKIP_TO_PREVIOUS) - ) - private val playAction = NotificationCompat.Action( - R.drawable.ic_play_notification_player, - context.getString(R.string.notification_play), - MediaButtonReceiver.buildMediaButtonPendingIntent(context, ACTION_PLAY) - ) - private val pauseAction = NotificationCompat.Action( - R.drawable.ic_pause_notification_player, - context.getString(R.string.notification_pause), - MediaButtonReceiver.buildMediaButtonPendingIntent(context, ACTION_PAUSE) - ) - - private val favoriteAction = NotificationCompat.Action( - R.drawable.ic_favorite_notification_player, - context.getString(R.string.notification_favorite), - PendingIntent.getBroadcast(context, - UUID.randomUUID().hashCode(), - Intent(context, MediaControlBroadcastReceiver::class.java).apply { - putExtra(FAVORITE, true) - }, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 - ) - ) - - private val unFavoriteAction = NotificationCompat.Action( - R.drawable.ic_unfavorite_notification_player, - context.getString(R.string.notification_unfavorite), - PendingIntent.getBroadcast( - context, - UUID.randomUUID().hashCode(), - Intent(context, MediaControlBroadcastReceiver::class.java).apply { - putExtra(FAVORITE, false) - }, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 - ) - ) - - private val skipToNextAction = NotificationCompat.Action( - R.drawable.ic_next_notification_player, - context.getString(R.string.notification_skip_to_next), - MediaButtonReceiver.buildMediaButtonPendingIntent(context, ACTION_SKIP_TO_NEXT) - ) - private val stopPendingIntent = - MediaButtonReceiver.buildMediaButtonPendingIntent(context, ACTION_STOP) - - - fun buildNotification( - mediaSession: MediaSessionCompat, - media: Media?, - onGoing: Boolean, - isPlayingExternal: Boolean?, - isFavorite: Boolean?, - mediaDuration: Long?, - art: Bitmap? - ): Notification { - if (shouldCreateNowPlayingChannel()) { - createNowPlayingChannel() - } - val playbackState = mediaSession.controller.playbackState - val builder = NotificationCompat.Builder(context, NOW_PLAYING_CHANNEL) - val actions = if (isFavorite == null) mutableListOf(0, 1, 2) else mutableListOf( - 0, - 2, - 3 - ) // favorite,play/pause,next - val duration = mediaDuration ?: 0L - val currentDuration = - mediaSession.controller.metadata.getLong(MediaMetadata.METADATA_KEY_DURATION) - val shouldUseMetadata = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - - isFavorite?.let { - builder.addAction(if (it) unFavoriteAction else favoriteAction) - } - - builder.addAction(skipToPreviousAction) - when { - isPlayingExternal != null -> { - if (isPlayingExternal) { - builder.addAction(pauseAction) - } else { - builder.addAction(playAction) - } - } - - playbackState.isPlaying -> { - Log.i("NotificationBuilder", "Player is playing... onGoing: $onGoing") - builder.addAction(pauseAction) - } - - playbackState.isPlayEnabled -> { - Log.i("NotificationBuilder", "Player is NOT playing... onGoing: $onGoing") - builder.addAction(playAction) - } - - else -> { - Log.i("NotificationBuilder", "ELSE") - builder.addAction(playAction) - } - } - - builder.addAction(skipToNextAction) - - val mediaStyle = MediaStyle() - .setCancelButtonIntent(stopPendingIntent) - .setShowActionsInCompactView(*actions.toIntArray()) - .setShowCancelButton(true) - .setMediaSession(mediaSession.sessionToken) - - /// when isAds is true, the metadata should be updated to show the correct value - val isAds = media?.name?.contains("Propaganda") ?: false - - if (shouldUseMetadata && (isAds || currentDuration != duration)) { - mediaSession.setMetadata( - MediaMetadataCompat.Builder() - .putString(MediaMetadata.METADATA_KEY_TITLE, media?.name ?: "Propaganda") - .putString(MediaMetadata.METADATA_KEY_ARTIST, media?.author ?: "") - .putBitmap( - MediaMetadata.METADATA_KEY_ALBUM_ART, art - ) - .putLong(MediaMetadata.METADATA_KEY_DURATION, duration) // 4 - .build() - ) - } - - - val notifyIntent = Intent("SUA_MUSICA_FLUTTER_NOTIFICATION_CLICK").apply { - addCategory(Intent.CATEGORY_DEFAULT) - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - } - val notifyPendingIntent = PendingIntent.getActivity( - context, - 0, - notifyIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT else PendingIntent.FLAG_UPDATE_CURRENT - ) - val notification = builder.apply { - setContentIntent(notifyPendingIntent) - setStyle(mediaStyle) - setCategory(NotificationCompat.CATEGORY_PROGRESS) - setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - setShowWhen(false) - setColorized(true) - setOnlyAlertOnce(false) - setAutoCancel(false) - setOngoing(onGoing) - setSmallIcon(R.drawable.ic_notification) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - setDefaults(Notification.DEFAULT_LIGHTS) - setVibrate(longArrayOf(0)) - } else { - setDefaults(Notification.DEFAULT_LIGHTS) - } - if (!shouldUseMetadata) { - setLargeIcon(art) - setContentTitle(media?.name ?: "Propaganda") - setContentText(media?.author ?: "") - } - - }.build() - - if (onGoing) { - notification.flags += Notification.FLAG_ONGOING_EVENT - notification.flags += Notification.FLAG_NO_CLEAR - } - - Log.i("NotificationBuilder", "Sending Notification onGoing: $onGoing") - - return notification - } - - private fun shouldCreateNowPlayingChannel() = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !nowPlayingChannelExists() - - @RequiresApi(Build.VERSION_CODES.O) - private fun nowPlayingChannelExists() = - platformNotificationManager.getNotificationChannel(NOW_PLAYING_CHANNEL) != null - - @RequiresApi(Build.VERSION_CODES.O) - private fun createNowPlayingChannel() { - val notificationChannel = NotificationChannel( - NOW_PLAYING_CHANNEL, - context.getString(R.string.notification_channel), - NotificationManager.IMPORTANCE_LOW - ) - .apply { - description = context.getString(R.string.notification_channel_description) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { - this.vibrationPattern = longArrayOf(0) - this.enableVibration(true) - } - } - - platformNotificationManager.createNotificationChannel(notificationChannel) - } -} - 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 995aa543..c9b8fa7f 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 com.google.android.exoplayer2.BuildConfig +import io.flutter.BuildConfig import org.xmlpull.v1.XmlPullParserException import java.io.IOException import java.security.MessageDigest diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlaybackStateCompatExt.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlaybackStateCompatExt.kt deleted file mode 100644 index de10d30e..00000000 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlaybackStateCompatExt.kt +++ /dev/null @@ -1,64 +0,0 @@ -package br.com.suamusica.player - -import android.os.SystemClock -import android.support.v4.media.session.PlaybackStateCompat - -/** - * Useful extension methods for [PlaybackStateCompat]. - */ -//TODO: Maybe remove this one -inline val PlaybackStateCompat.isLoading - get() = (state == PlaybackStateCompat.STATE_BUFFERING) || - (state == PlaybackStateCompat.STATE_CONNECTING) - -inline val PlaybackStateCompat.isPrepared - get() = (state == PlaybackStateCompat.STATE_BUFFERING) || - (state == PlaybackStateCompat.STATE_PLAYING) || - (state == PlaybackStateCompat.STATE_PAUSED) - -inline val PlaybackStateCompat.isPlaying - get() = (state == PlaybackStateCompat.STATE_BUFFERING && currentPlayBackPosition > 0) || - (state == PlaybackStateCompat.STATE_PLAYING) - -inline val PlaybackStateCompat.isPlayEnabled - get() = (actions and PlaybackStateCompat.ACTION_PLAY != 0L) || - ((actions and PlaybackStateCompat.ACTION_PLAY_PAUSE != 0L) && - (state == PlaybackStateCompat.STATE_PAUSED)) - -inline val PlaybackStateCompat.isPauseEnabled - get() = (actions and PlaybackStateCompat.ACTION_PAUSE != 0L) || - ((actions and PlaybackStateCompat.ACTION_PLAY_PAUSE != 0L) && - (state == PlaybackStateCompat.STATE_BUFFERING || - state == PlaybackStateCompat.STATE_PLAYING)) - -inline val PlaybackStateCompat.isSkipToNextEnabled - get() = actions and PlaybackStateCompat.ACTION_SKIP_TO_NEXT != 0L - -inline val PlaybackStateCompat.isSkipToPreviousEnabled - get() = actions and PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS != 0L - -inline val PlaybackStateCompat.stateName - get() = when (state) { - PlaybackStateCompat.STATE_NONE -> "STATE_NONE" - PlaybackStateCompat.STATE_STOPPED -> "STATE_STOPPED" - PlaybackStateCompat.STATE_PAUSED -> "STATE_PAUSED" - PlaybackStateCompat.STATE_PLAYING -> "STATE_PLAYING" - PlaybackStateCompat.STATE_FAST_FORWARDING -> "STATE_FAST_FORWARDING" - PlaybackStateCompat.STATE_REWINDING -> "STATE_REWINDING" - PlaybackStateCompat.STATE_BUFFERING -> "STATE_BUFFERING" - PlaybackStateCompat.STATE_ERROR -> "STATE_ERROR" - else -> "UNKNOWN_STATE" - } - -/** - * Calculates the current playback position based on last update time along with playback - * state and speed. - */ -inline val PlaybackStateCompat.currentPlayBackPosition: Long - get() = if (state == PlaybackStateCompat.STATE_PLAYING) { - val timeDelta = SystemClock.elapsedRealtime() - lastPositionUpdateTime - (position + (timeDelta * playbackSpeed)).toLong() - } else { - position - } - 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 d133fbe0..b05d715d 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 @@ -2,20 +2,29 @@ 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.* class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { - fun notifyStateChange(state: Int, error: String? = null) { + fun notifyStateChange(state: @State Int) { val playerState = when (state) { - PlaybackStateCompat.STATE_NONE -> PlayerState.IDLE - PlaybackStateCompat.STATE_BUFFERING -> PlayerState.BUFFERING - PlaybackStateCompat.STATE_PAUSED -> PlayerState.PAUSED - PlaybackStateCompat.STATE_PLAYING -> PlayerState.PLAYING - PlaybackStateCompat.STATE_ERROR -> PlayerState.ERROR - PlaybackStateCompat.STATE_STOPPED -> PlayerState.COMPLETED + STATE_IDLE, STATE_READY -> PlayerState.IDLE + STATE_BUFFERING -> PlayerState.BUFFERING + STATE_ENDED -> PlayerState.COMPLETED + STATE_READY -> PlayerState.STATE_READY else -> PlayerState.IDLE } - Log.i("Player", "Notifying Player State change: $playerState") - channelManager.notifyPlayerStateChange("sua-musica-player", playerState, error) + Log.i("Player", "#NATIVE LOGS ==> Notifying Player State change: $playerState | $state") + channelManager.notifyPlayerStateChange("sua-musica-player", playerState) + } + + fun notifyPlaying(isPlaying:Boolean){ + channelManager.notifyPlayerStateChange("sua-musica-player", if(isPlaying) PlayerState.PLAYING else PlayerState.PAUSED) + } + + fun notifyError(message: String? = null){ + Log.i("Player", "Notifying Error: $message") + channelManager.notifyError("sua-musica-player", PlayerState.ERROR, message) } fun notifySeekEnd() { @@ -31,9 +40,25 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { Log.i("Player", "Notifying Player Previous") channelManager.notifyPrevious("sua-musica-player") } + fun notifyItemTransition(from:String) { + Log.i("Player", "#NATIVE LOGS ==> notifyItemTransition | FROM: $from") + channelManager.notifyItemTransition("sua-musica-player") + } + fun currentMediaIndex(currentMediaIndex: Int, from: String) { + Log.i("Player", "#NATIVE LOGS ==> 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") + channelManager.onRepeatChanged("sua-musica-player", repeatMode) + } + fun onShuffleModeEnabled(shuffleModeEnabled: Boolean) { + Log.i("Player", "Notifying Player onRepeatChanged: $shuffleModeEnabled") + channelManager.onShuffleModeEnabled("sua-musica-player", shuffleModeEnabled) + } } \ 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 913b73fb..24cb0d5b 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,6 +1,8 @@ package br.com.suamusica.player import android.util.Log +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -9,7 +11,7 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { +class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { companion object { // Argument names @@ -20,16 +22,38 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { const val BIG_COVER_URL_ARGUMENT = "bigCoverUrl" const val IS_PLAYING_ARGUMENT = "isPlaying" const val IS_FAVORITE_ARGUMENT = "isFavorite" + const val FALLBACK_URL = "fallbackURL" + const val ID_FAVORITE_ARGUMENT = "idFavorite" + const val NEW_URI_ARGUMENT = "newUri" + const val ID_URI_ARGUMENT = "idUri" const val POSITION_ARGUMENT = "position" + const val TIME_POSITION_ARGUMENT = "timePosition" + const val INDEXES_TO_REMOVE = "indexesToDelete" + const val POSITIONS_LIST = "positionsList" const val LOAD_ONLY = "loadOnly" const val RELEASE_MODE_ARGUMENT = "releaseMode" private const val CHANNEL = "suamusica.com.br/player" + const val FAVORITE: String = "favorite" // Method names - const val LOAD_METHOD = "load" const val PLAY_METHOD = "play" + const val SET_REPEAT_MODE = "set_repeat_mode" + const val ENQUEUE = "enqueue" + const val REMOVE_ALL = "remove_all" + const val REMOVE_IN = "remove_in" + const val REORDER = "reorder" + const val PLAY_FROM_QUEUE_METHOD = "playFromQueue" const val RESUME_METHOD = "resume" const val PAUSE_METHOD = "pause" + const val NEXT_METHOD = "next" + const val PREVIOUS_METHOD = "previous" + 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" const val STOP_METHOD = "stop" const val RELEASE_METHOD = "release" const val SEEK_METHOD = "seek" @@ -39,12 +63,12 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { const val GET_CURRENT_POSITION_METHOD = "getCurrentPosition" const val SET_RELEASE_MODE_METHOD = "setReleaseMode" const val CAN_PLAY = "can_play" - const val SEND_NOTIFICATION = "send_notification" const val DISABLE_NOTIFICATION_COMMANDS = "disable_notification_commands" const val ENABLE_NOTIFICATION_COMMANDS = "enable_notification_commands" const val TAG = "Player" const val Ok = 1 private var alreadyAttachedToActivity: Boolean = false + var cookie = "" } override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { @@ -74,13 +98,16 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { override fun onDetachedFromActivityForConfigChanges() { Log.d(TAG, "onDetachedFromActivityForConfigChanges") } + override fun onReattachedToActivityForConfigChanges(p0: ActivityPluginBinding) { Log.d(TAG, "onReattachedToActivityForConfigChanges") } + override fun onDetachedFromActivity() { Log.d(TAG, "onDetachedFromActivity") alreadyAttachedToActivity = false } + override fun onMethodCall(call: MethodCall, response: MethodChannel.Result) { try { handleMethodCall(call, response) @@ -91,103 +118,174 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { } private fun handleMethodCall(call: MethodCall, response: MethodChannel.Result) { - val cookie = call.argument("cookie") - PlayerSingleton.externalPlayback = call.argument("externalplayback") - Log.d(TAG, "method: ${call.method} cookie: $cookie externalPlayback: ${PlayerSingleton.externalPlayback}") + if (call.method == ENQUEUE) { + 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) { - LOAD_METHOD -> { - val name = call.argument(NAME_ARGUMENT)!! - val author = call.argument(AUTHOR_ARGUMENT)!! - val url = call.argument(URL_ARGUMENT)!! - val coverUrl = call.argument(COVER_URL_ARGUMENT)!! - val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) - val position = call.argument(POSITION_ARGUMENT) - val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) - - PlayerSingleton.mediaSessionConnection?.prepare(cookie!!, Media(name, author, url, coverUrl,bigCoverUrl, isFavorite)) - position?.let { - PlayerSingleton.mediaSessionConnection?.seek(it.toLong(), false) - } - PlayerSingleton.mediaSessionConnection?.sendNotification(name, author, url, coverUrl, null, bigCoverUrl,isFavorite) - Log.d(TAG, "method: ${call.method} name: $name author: $author") + ENQUEUE -> { + 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, + ) } - SEND_NOTIFICATION -> { - val name = call.argument(NAME_ARGUMENT)!! - val author = call.argument(AUTHOR_ARGUMENT)!! - val url = call.argument(URL_ARGUMENT)!! - val coverUrl = call.argument(COVER_URL_ARGUMENT)!! - val isPlaying: Boolean? = call.argument(IS_PLAYING_ARGUMENT) - val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) - val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) - PlayerSingleton.mediaSessionConnection?.sendNotification(name, author, url, coverUrl, isPlaying, bigCoverUrl, isFavorite) - } PLAY_METHOD -> { - val name = call.argument(NAME_ARGUMENT)!! - val author = call.argument(AUTHOR_ARGUMENT)!! - val url = call.argument(URL_ARGUMENT)!! - val coverUrl = call.argument(COVER_URL_ARGUMENT)!! - val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) - val position = call.argument(POSITION_ARGUMENT) - val loadOnly = call.argument(LOAD_ONLY)!! - val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) - - PlayerSingleton.mediaSessionConnection?.prepare(cookie!!, Media(name, author, url, coverUrl, bigCoverUrl, isFavorite)) - Log.d(TAG, "before prepare: cookie: $cookie") - position?.let { - PlayerSingleton.mediaSessionConnection?.seek(it.toLong(), true) + val shouldPrepare = call.argument("shouldPrepare") ?: false + PlayerSingleton.mediaSessionConnection?.play(shouldPrepare) + } + + SET_REPEAT_MODE -> { + val mode = call.argument("mode") ?: "" + PlayerSingleton.mediaSessionConnection?.setRepeatMode(mode) + } + + REORDER -> { + val from = call.argument("oldIndex") + val to = call.argument("newIndex") + val positionsList = + call.argument>>(POSITIONS_LIST) ?: emptyList() + if (from != null && to != null) { + PlayerSingleton.mediaSessionConnection?.reorder(from, to, positionsList) } + } + + REMOVE_ALL -> { + PlayerSingleton.mediaSessionConnection?.removeAll() + } + + REMOVE_IN -> { + val indexes = call.argument>(INDEXES_TO_REMOVE) ?: emptyList() + PlayerSingleton.mediaSessionConnection?.removeIn(indexes) + } + + NEXT_METHOD -> { + PlayerSingleton.mediaSessionConnection?.next() + } + + TOGGLE_SHUFFLE -> { + val positionsList = + call.argument>>(POSITIONS_LIST) ?: emptyList() + PlayerSingleton.mediaSessionConnection?.toggleShuffle(positionsList) + } + + UPDATE_MEDIA_URI -> { + val id = call.argument("id") ?: 0 + val uri = call.argument("uri") + PlayerSingleton.mediaSessionConnection?.updateMediaUri(id, uri) + } + + REPEAT_MODE -> { + PlayerSingleton.mediaSessionConnection?.repeatMode() + } + + DISABLE_REPEAT_MODE -> { + PlayerSingleton.mediaSessionConnection?.disableRepeatMode() + } - if (!loadOnly) { - PlayerSingleton.mediaSessionConnection?.play() + PREVIOUS_METHOD -> { + PlayerSingleton.mediaSessionConnection?.previous() + } + + UPDATE_NOTIFICATION -> { + 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 + PlayerSingleton.mediaSessionConnection?.playFromQueue( + position, + timePosition.toLong(), + loadOnly + ) + } + RESUME_METHOD -> { PlayerSingleton.mediaSessionConnection?.play() } + PAUSE_METHOD -> { PlayerSingleton.mediaSessionConnection?.pause() } + "ads_playing" -> { PlayerSingleton.mediaSessionConnection?.adsPlaying() } + STOP_METHOD -> { PlayerSingleton.mediaSessionConnection?.stop() } + RELEASE_METHOD -> { PlayerSingleton.mediaSessionConnection?.release() } + SEEK_METHOD -> { val position = call.argument(POSITION_ARGUMENT)!! PlayerSingleton.mediaSessionConnection?.seek(position, true) } + REMOVE_NOTIFICATION_METHOD -> { PlayerSingleton.mediaSessionConnection?.removeNotification() } + SET_VOLUME_METHOD -> { } + GET_DURATION_METHOD -> { response.success(PlayerSingleton.mediaSessionConnection?.duration) return } + GET_CURRENT_POSITION_METHOD -> { response.success(PlayerSingleton.mediaSessionConnection?.currentPosition) return } + SET_RELEASE_MODE_METHOD -> { val releaseModeName = call.argument(RELEASE_MODE_ARGUMENT) - val releaseMode = ReleaseMode.valueOf(releaseModeName!!.substring("ReleaseMode.".length)) + val releaseMode = + ReleaseMode.valueOf(releaseModeName!!.substring("ReleaseMode.".length)) PlayerSingleton.mediaSessionConnection?.releaseMode = releaseMode.ordinal } + DISABLE_NOTIFICATION_COMMANDS -> { } + ENABLE_NOTIFICATION_COMMANDS -> { } + CAN_PLAY -> { // no operation required on Android } + else -> { response.notImplemented() return 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 77979ad4..e0d9e51e 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 @@ -8,14 +8,15 @@ object PlayerSingleton { var channel: MethodChannel? = null var mediaSessionConnection: MediaSessionConnection? = null var externalPlayback: Boolean? = false - var lastFavorite: Boolean=false private const val TAG = "Player" + var playerChangeNotifier: PlayerChangeNotifier? = null fun setChannel(c: MethodChannel, context: Context) { channel = c + playerChangeNotifier = PlayerChangeNotifier(MethodChannelManager(c)) mediaSessionConnection = MediaSessionConnection( context, - PlayerChangeNotifier(MethodChannelManager(c)) + playerChangeNotifier!! ) } @@ -32,9 +33,9 @@ object PlayerSingleton { mediaSessionConnection?.togglePlayPause() channel?.invokeMethod("commandCenter.onTogglePlayPause", emptyMap()) } - fun adsPlaying(){ - mediaSessionConnection?.adsPlaying() - } +// fun adsPlaying(){ +// mediaSessionConnection?.adsPlaying() +// } fun pause() { if (externalPlayback!!) { channel?.invokeMethod("externalPlayback.pause", emptyMap()) @@ -58,10 +59,9 @@ object PlayerSingleton { fun favorite(shouldFavorite: Boolean) { Log.d(TAG, "Should Favorite: $shouldFavorite") - lastFavorite = shouldFavorite mediaSessionConnection?.favorite(shouldFavorite) val args = mutableMapOf() - args[FAVORITE] = shouldFavorite + args[PlayerPlugin.FAVORITE] = shouldFavorite channel?.invokeMethod("commandCenter.onFavorite", args) } } \ No newline at end of file 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 439efa8e..631d10f6 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 @@ -9,5 +9,7 @@ enum class PlayerState { COMPLETED, ERROR, SEEK_END, - BUFFER_EMPTY + BUFFER_EMPTY, + ITEM_TRANSITION, + STATE_READY, } \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PreviousActionProvider.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PreviousActionProvider.kt deleted file mode 100644 index dc5a58a5..00000000 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PreviousActionProvider.kt +++ /dev/null @@ -1,23 +0,0 @@ -package br.com.suamusica.player - -import android.os.Bundle -import android.support.v4.media.session.PlaybackStateCompat -import android.util.Log -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector - -class PreviousActionProvider : - MediaSessionConnector.CustomActionProvider { - - override fun onCustomAction(player: Player, action: String, extras: Bundle?) { - PlayerSingleton.previous() - } - - override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? { - return PlaybackStateCompat.CustomAction.Builder( - "Voltar a música anterior", - "Voltar a música anterior", - R.drawable.ic_prev_notification_player, - ).build() - } -} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/CustomHlsMasterPlaylist.java b/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/CustomHlsMasterPlaylist.java index d37ad30c..36292a55 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/CustomHlsMasterPlaylist.java +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/CustomHlsMasterPlaylist.java @@ -4,21 +4,25 @@ import androidx.annotation.Nullable; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.util.MimeTypes; +import androidx.media3.common.Format; +import androidx.media3.common.DrmInitData; +import androidx.media3.common.StreamKey; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.hls.playlist.HlsPlaylist; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; - +@UnstableApi public final class CustomHlsMasterPlaylist extends HlsPlaylist { - /** Represents an empty master playlist, from which no attributes can be inherited. */ + /** + * Represents an empty master playlist, from which no attributes can be inherited. + */ + public static final CustomHlsMasterPlaylist EMPTY = new CustomHlsMasterPlaylist( /* baseUri= */ "", @@ -39,35 +43,52 @@ public final class CustomHlsMasterPlaylist extends HlsPlaylist { public static final int GROUP_INDEX_AUDIO = 1; public static final int GROUP_INDEX_SUBTITLE = 2; - /** A variant (i.e. an #EXT-X-STREAM-INF tag) in a master playlist. */ + /** + * A variant (i.e. an #EXT-X-STREAM-INF tag) in a master playlist. + */ public static final class Variant { - /** The variant's url. */ + /** + * The variant's url. + */ public final Uri url; - /** Format information associated with this variant. */ + /** + * Format information associated with this variant. + */ public final Format format; - /** The video rendition group referenced by this variant, or {@code null}. */ + /** + * The video rendition group referenced by this variant, or {@code null}. + */ @Nullable public final String videoGroupId; - /** The audio rendition group referenced by this variant, or {@code null}. */ - @Nullable public final String audioGroupId; + /** + * The audio rendition group referenced by this variant, or {@code null}. + */ + @Nullable + public final String audioGroupId; - /** The subtitle rendition group referenced by this variant, or {@code null}. */ - @Nullable public final String subtitleGroupId; + /** + * The subtitle rendition group referenced by this variant, or {@code null}. + */ + @Nullable + public final String subtitleGroupId; - /** The caption rendition group referenced by this variant, or {@code null}. */ - @Nullable public final String captionGroupId; + /** + * The caption rendition group referenced by this variant, or {@code null}. + */ + @Nullable + public final String captionGroupId; /** - * @param url See {@link #url}. - * @param format See {@link #format}. - * @param videoGroupId See {@link #videoGroupId}. - * @param audioGroupId See {@link #audioGroupId}. + * @param url See {@link #url}. + * @param format See {@link #format}. + * @param videoGroupId See {@link #videoGroupId}. + * @param audioGroupId See {@link #audioGroupId}. * @param subtitleGroupId See {@link #subtitleGroupId}. - * @param captionGroupId See {@link #captionGroupId}. + * @param captionGroupId See {@link #captionGroupId}. */ public Variant( Uri url, @@ -90,6 +111,7 @@ public Variant( * @param url The media playlist url. * @return The variant instance. */ + public static CustomHlsMasterPlaylist.Variant createMediaPlaylistVariantUrl(Uri url) { Format format = new Format.Builder() @@ -113,32 +135,45 @@ public static CustomHlsMasterPlaylist.Variant createMediaPlaylistVariantUrl(Uri /* captionGroupId= */ null); } - /** Returns a copy of this instance with the given {@link Format}. */ + /** + * Returns a copy of this instance with the given {@link Format}. + */ public CustomHlsMasterPlaylist.Variant copyWithFormat(Format format) { return new CustomHlsMasterPlaylist.Variant(url, format, videoGroupId, audioGroupId, subtitleGroupId, captionGroupId); } } - /** A rendition (i.e. an #EXT-X-MEDIA tag) in a master playlist. */ + /** + * A rendition (i.e. an #EXT-X-MEDIA tag) in a master playlist. + */ public static final class Rendition { - /** The rendition's url, or null if the tag does not have a URI attribute. */ - @Nullable public final Uri url; + /** + * The rendition's url, or null if the tag does not have a URI attribute. + */ + @Nullable + public final Uri url; - /** Format information associated with this rendition. */ + /** + * Format information associated with this rendition. + */ public final Format format; - /** The group to which this rendition belongs. */ + /** + * The group to which this rendition belongs. + */ public final String groupId; - /** The name of the rendition. */ + /** + * The name of the rendition. + */ public final String name; /** - * @param url See {@link #url}. - * @param format See {@link #format}. + * @param url See {@link #url}. + * @param format See {@link #format}. * @param groupId See {@link #groupId}. - * @param name See {@link #name}. + * @param name See {@link #name}. */ public Rendition(@Nullable Uri url, Format format, String groupId, String name) { this.url = url; @@ -149,17 +184,29 @@ public Rendition(@Nullable Uri url, Format format, String groupId, String name) } - /** All of the media playlist URLs referenced by the playlist. */ + /** + * All of the media playlist URLs referenced by the playlist. + */ public final List mediaPlaylistUrls; - /** The variants declared by the playlist. */ + /** + * The variants declared by the playlist. + */ public final List variants; - /** The video renditions declared by the playlist. */ + /** + * The video renditions declared by the playlist. + */ public final List videos; - /** The audio renditions declared by the playlist. */ + /** + * The audio renditions declared by the playlist. + */ public final List audios; - /** The subtitle renditions declared by the playlist. */ + /** + * The subtitle renditions declared by the playlist. + */ public final List subtitles; - /** The closed caption renditions declared by the playlist. */ + /** + * The closed caption renditions declared by the playlist. + */ public final List closedCaptions; /** @@ -173,25 +220,30 @@ public Rendition(@Nullable Uri url, Format format, String groupId, String name) * captions information. */ public final List muxedCaptionFormats; - /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */ + /** + * Contains variable definitions, as defined by the #EXT-X-DEFINE tag. + */ public final Map variableDefinitions; - /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */ + /** + * DRM initialization data derived from #EXT-X-SESSION-KEY tags. + */ public final List sessionKeyDrmInitData; /** - * @param baseUri See {@link #baseUri}. - * @param tags See {@link #tags}. - * @param variants See {@link #variants}. - * @param videos See {@link #videos}. - * @param audios See {@link #audios}. - * @param subtitles See {@link #subtitles}. - * @param closedCaptions See {@link #closedCaptions}. - * @param muxedAudioFormat See {@link #muxedAudioFormat}. - * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. + * @param variants See {@link #variants}. + * @param videos See {@link #videos}. + * @param audios See {@link #audios}. + * @param subtitles See {@link #subtitles}. + * @param closedCaptions See {@link #closedCaptions}. + * @param muxedAudioFormat See {@link #muxedAudioFormat}. + * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. * @param hasIndependentSegments See {@link #hasIndependentSegments}. - * @param variableDefinitions See {@link #variableDefinitions}. - * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}. + * @param variableDefinitions See {@link #variableDefinitions}. + * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}. */ + public CustomHlsMasterPlaylist( String baseUri, List tags, @@ -222,6 +274,7 @@ public CustomHlsMasterPlaylist( } @Override + public CustomHlsMasterPlaylist copy(List streamKeys) { return new CustomHlsMasterPlaylist( baseUri, @@ -246,6 +299,7 @@ public CustomHlsMasterPlaylist copy(List streamKeys) { * @param variantUrl The url of the single variant. * @return A master playlist with a single variant for the provided url. */ + public static CustomHlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) { List variant = Collections.singletonList(CustomHlsMasterPlaylist.Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl))); @@ -293,6 +347,7 @@ private static void addMediaPlaylistUrls(List } } + private static List copyStreams( List streams, int groupIndex, List streamKeys) { List copiedStreams = new ArrayList<>(streamKeys.size()); diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/CustomHlsPlaylistParser.java b/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/CustomHlsPlaylistParser.java index 531b73e4..81f20611 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/CustomHlsPlaylistParser.java +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/CustomHlsPlaylistParser.java @@ -17,21 +17,26 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.source.UnrecognizedInputFormatException; -import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.UriUtil; -import com.google.android.exoplayer2.util.Util; +import androidx.annotation.OptIn; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.ParserException; +import androidx.media3.common.DrmInitData; +//import androidx.media3.exoplayer.extractor.mp4.PsshAtomUtil; +import androidx.media3.common.Metadata; +import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.UriUtil; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.source.UnrecognizedInputFormatException; +import androidx.media3.exoplayer.hls.HlsTrackMetadataEntry; +import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist; +import androidx.media3.exoplayer.hls.playlist.HlsPlaylist; +import androidx.media3.exoplayer.upstream.ParsingLoadable; +//import androidx.media3.exoplayer.util.Assertions; +import androidx.media3.common.MimeTypes; +//import com.google.android.exoplayer2.util.UriUtil; +//import com.google.android.exoplayer2.util.Util; import android.util.Log; @@ -40,7 +45,9 @@ import android.util.Base64; import androidx.annotation.Nullable; +import androidx.media3.extractor.mp4.PsshAtomUtil; +@UnstableApi public class CustomHlsPlaylistParser implements ParsingLoadable.Parser { private static final String PLAYLIST_HEADER = "#EXTM3U"; @@ -171,6 +178,7 @@ public CustomHlsPlaylistParser(CustomHlsMasterPlaylist masterPlaylist) { this.masterPlaylist = masterPlaylist; } + @OptIn(markerClass = UnstableApi.class) @Override public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { Log.i("MusicService", "Player : Parser..."); @@ -211,6 +219,7 @@ public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { throw ParserException.createForUnsupportedContainerFeature("Failed to parse the playlist, could not identify any tags."); } + private static boolean checkPlaylistHeader(BufferedReader reader) throws IOException { int last = reader.read(); if (last == 0xEF) { @@ -232,6 +241,7 @@ private static boolean checkPlaylistHeader(BufferedReader reader) throws IOExcep return Util.isLinebreak(last); } + private static int skipIgnorableWhitespace(BufferedReader reader, boolean skipLinebreaks, int c) throws IOException { while (c != -1 && Character.isWhitespace(c) && (skipLinebreaks || !Util.isLinebreak(c))) { @@ -240,7 +250,8 @@ private static int skipIgnorableWhitespace(BufferedReader reader, boolean skipLi return c; } - private static CustomHlsMasterPlaylist parseMasterPlaylist(CustomHlsPlaylistParser.LineIterator iterator, String baseUri) + @UnstableApi + private static HlsPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri) throws IOException { HashMap> urlToVariantInfos = new HashMap<>(); HashMap variableDefinitions = new HashMap<>(); @@ -421,7 +432,7 @@ private static CustomHlsMasterPlaylist parseMasterPlaylist(CustomHlsPlaylistPars .setHeight(height) .setFrameRate(frameRate) .build(); - //.copyWithMetadata(metadata); + //.copyWithMetadata(metadata); if (uri == null) { // TODO: Remove this case and add a Rendition with a null uri to videos. } else { @@ -586,7 +597,7 @@ private static HlsMediaPlaylist parseMediaPlaylist( DrmInitData cachedDrmInitData = null; List trailingParts = new ArrayList<>(); Map renditionReports = new HashMap<>(); - HlsMediaPlaylist.ServerControl serverControl = new HlsMediaPlaylist.ServerControl(0,false,0,0,false); + HlsMediaPlaylist.ServerControl serverControl = new HlsMediaPlaylist.ServerControl(0, false, 0, 0, false); List updatedParts = new ArrayList<>(); String line; @@ -806,6 +817,7 @@ private static int parseSelectionFlags(String line) { } @C.RoleFlags + private static int parseRoleFlags(String line, Map variableDefinitions) { String concatenatedCharacteristics = parseOptionalStringAttr(line, REGEX_CHARACTERISTICS, variableDefinitions); @@ -829,6 +841,7 @@ private static int parseRoleFlags(String line, Map variableDefin return roleFlags; } + private static int parseChannelsAttribute(String line, Map variableDefinitions) { String channelsString = parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions); return channelsString != null @@ -837,6 +850,7 @@ private static int parseChannelsAttribute(String line, Map varia } @Nullable + private static DrmInitData.SchemeData parseDrmSchemeData( String line, String keyFormat, Map variableDefinitions) throws ParserException { @@ -859,24 +873,29 @@ private static DrmInitData.SchemeData parseDrmSchemeData( return null; } + private static String parseEncryptionScheme(String method) { return METHOD_SAMPLE_AES_CENC.equals(method) || METHOD_SAMPLE_AES_CTR.equals(method) ? C.CENC_TYPE_cenc : C.CENC_TYPE_cbcs; } + private static int parseIntAttr(String line, Pattern pattern) throws ParserException { return Integer.parseInt(parseStringAttr(line, pattern, new HashMap())); } + private static long parseLongAttr(String line, Pattern pattern) throws ParserException { return Long.parseLong(parseStringAttr(line, pattern, new HashMap())); } + private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException { return Double.parseDouble(parseStringAttr(line, pattern, new HashMap())); } + private static String parseStringAttr( String line, Pattern pattern, Map variableDefinitions) throws ParserException { diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/SMHlsPlaylistParserFactory.java b/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/SMHlsPlaylistParserFactory.java index 91b425aa..dd085fac 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/SMHlsPlaylistParserFactory.java +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/media/parser/SMHlsPlaylistParserFactory.java @@ -1,23 +1,28 @@ package br.com.suamusica.player.media.parser; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Collections; import java.util.List; -import com.google.android.exoplayer2.offline.FilteringManifestParser; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory; -import com.google.android.exoplayer2.upstream.ParsingLoadable; +import androidx.media3.common.StreamKey; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist; +import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist; +import androidx.media3.exoplayer.hls.playlist.HlsPlaylist; +import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParserFactory; +import androidx.media3.exoplayer.offline.FilteringManifestParser; +import androidx.media3.exoplayer.upstream.ParsingLoadable; +@UnstableApi public final class SMHlsPlaylistParserFactory implements HlsPlaylistParserFactory { private final List streamKeys; - /** Creates an instance that does not filter any parsing results. */ + /** + * Creates an instance that does not filter any parsing results. + */ public SMHlsPlaylistParserFactory() { this(Collections.emptyList()); } @@ -26,20 +31,21 @@ public SMHlsPlaylistParserFactory() { * Creates an instance that filters the parsing results using the given {@code streamKeys}. * * @param streamKeys See {@link - * FilteringManifestParser#FilteringManifestParser(ParsingLoadable.Parser, List)}. + * FilteringManifestParser#FilteringManifestParser(ParsingLoadable.Parser, List)}. */ public SMHlsPlaylistParserFactory(List streamKeys) { this.streamKeys = streamKeys; } + @NonNull @Override public ParsingLoadable.Parser createPlaylistParser() { return new FilteringManifestParser<>(new CustomHlsPlaylistParser(), streamKeys); } @Override - public ParsingLoadable.Parser createPlaylistParser(HlsMultivariantPlaylist multivariantPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) { + @NonNull + public ParsingLoadable.Parser createPlaylistParser(@Nullable HlsMultivariantPlaylist hlsMultivariantPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) { return new FilteringManifestParser<>(new CustomHlsPlaylistParser(), streamKeys); } - } diff --git a/packages/player/android/src/main/res/values/strings.xml b/packages/player/android/src/main/res/values/strings.xml index 846f8c42..f902887e 100644 --- a/packages/player/android/src/main/res/values/strings.xml +++ b/packages/player/android/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ Media SM - Tocando Agora + SM_NOW_PLAYING Exibe que música está tocando no Sua Música diff --git a/packages/player/example/.flutter-plugins-dependencies b/packages/player/example/.flutter-plugins-dependencies index 069a4193..ca9c0226 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/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/suamusica/projects/suamusica/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"android":[{"name":"isar_flutter_libs","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_android-2.2.2/","native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/suamusica/projects/suamusica/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"macos":[{"name":"isar_flutter_libs","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"isar_flutter_libs","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"isar_flutter_libs","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/","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-06-24 09:57:26.814269","version":"3.22.2"} \ No newline at end of file +{"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 diff --git a/packages/player/example/android/app/build.gradle b/packages/player/example/android/app/build.gradle index 2ed9ff85..9d68c793 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 28 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -39,8 +39,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "br.com.suamusica.suamusica_player_example" - minSdkVersion 16 - targetSdkVersion 30 + minSdkVersion 21 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -54,9 +54,16 @@ android { } } - compileOptions { + + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = '1.8' + } + namespace 'br.com.suamusica.suamusica_player_example' } flutter { @@ -65,7 +72,15 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support:multidex:1.0.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + testImplementation 'junit:junit:4.13.2' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' + api ('com.google.ads.interactivemedia.v3:interactivemedia:'){ + version { + strictly '3.33.0' + } + } } diff --git a/packages/player/example/android/app/src/main/AndroidManifest.xml b/packages/player/example/android/app/src/main/AndroidManifest.xml index b23c5249..c9991d80 100644 --- a/packages/player/example/android/app/src/main/AndroidManifest.xml +++ b/packages/player/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + xmlns:tools="http://schemas.android.com/tools"> @@ -17,7 +17,8 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustResize" + android:exported="true">