diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index 7505b818..ab5a5768 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.0' 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.media3:media3-exoplayer:$media3_version" implementation "androidx.media:media:1.7.0" implementation "org.jetbrains.kotlin:kotlin-reflect" + // Glide dependencies implementation "com.github.bumptech.glide:glide:4.12.0" -// implementation files('/Users/alantrope/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar') + 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/suamusica/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar') kapt "com.github.bumptech.glide:compiler:4.12.0" } 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..57ec7ce7 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/MediaButtonEventHandler.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt index 5c43182e..43dc15b1 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,246 @@ 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_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 com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture -class MediaButtonEventHandler : MediaSessionConnector.MediaButtonEventHandler { +@UnstableApi +class MediaButtonEventHandler( + private val mediaService: MediaService?, +) : MediaLibraryService.MediaLibrarySession.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("prepare", 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)) + }.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) + .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" + buildIcons(isFavorite) + PlayerSingleton.favorite(isFavorite) + } - if (intent == null) { - return + if (customCommand.customAction == "seek") { + mediaService?.seek(args.getLong("position"), args.getBoolean("playWhenReady")) } - if (Intent.ACTION_MEDIA_BUTTON == intent.action) { - mediaButtonHandler(intent) - } else if (intent.hasExtra(FAVORITE)) { - PlayerSingleton.favorite(intent.getBooleanExtra(FAVORITE, false)) + if (customCommand.customAction == "onTogglePlayPause") { + mediaService?.togglePlayPause() } - } + if (customCommand.customAction == "stop") { + mediaService?.stop() + } - private fun mediaButtonHandler(intent: Intent) { - val ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) - Log.d("Player", "Key: $ke") + if (customCommand.customAction == "send_notification") { + args.let { + val isFavorite = it.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT) + buildIcons(isFavorite) - if (ke!!.action == KeyEvent.ACTION_UP) { - return + } + } + if (customCommand.customAction == "play") { + mediaService?.play() + } + if (customCommand.customAction == "notification_previous") { + PlayerSingleton.previous() + } + if (customCommand.customAction == "notification_next") { + PlayerSingleton.next() + } + if (customCommand.customAction == "pause") { + mediaService?.pause() + } + if (customCommand.customAction == "ads_playing" || customCommand.customAction == "remove_notification") { + mediaService?.removeNotification() } - when (ke.keyCode) { - KeyEvent.KEYCODE_MEDIA_PLAY -> { - PlayerSingleton.play() + if (customCommand.customAction == "prepare") { + args.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) + ) + buildIcons(isFavorite ?: false) } - KeyEvent.KEYCODE_MEDIA_PAUSE -> { - PlayerSingleton.pause() - } - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { - Log.d("Player", "Player: Key Code : PlayPause") - PlayerSingleton.togglePlayPause() + } + return Futures.immediateFuture( + SessionResult(SessionResult.RESULT_SUCCESS) + ) + } + + private fun buildIcons(isFavorite: Boolean): Unit? { + val list = ImmutableList.of( + 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(), + CommandButton.Builder() + .setDisplayName("notification_next") + .setIconResId(drawable.media3_icon_next) + .setSessionCommand(SessionCommand("notification_next", Bundle.EMPTY)) + .setEnabled(true) + .build(), + ) + return mediaService?.mediaSession?.setCustomLayout( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + list.plus( + CommandButton.Builder() + .setDisplayName("notification_previous") + .setIconResId(drawable.media3_icon_previous) + .setSessionCommand(SessionCommand("notification_previous", Bundle.EMPTY)) + .build() + ) + } else { + list } - KeyEvent.KEYCODE_MEDIA_NEXT -> { - Log.d("Player", "Player: Key Code : Next") - PlayerSingleton.next() + ) + } + + + @UnstableApi + override fun onMediaButtonEvent( + session: MediaSession, + controllerInfo: MediaSession.ControllerInfo, + intent: Intent + ): Boolean { + onMediaButtonEventHandler(intent) + return true + } + + @UnstableApi + fun onMediaButtonEventHandler(intent: Intent?) { + + if (intent == null) { + return + } + + 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") + mediaService?.togglePlayPause() + } + + KeyEvent.KEYCODE_MEDIA_NEXT -> { + Log.d("Player", "Player: Key Code : Next") + PlayerSingleton.next() + } + + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { + Log.d("Player", "Player: Key Code : Previous") + PlayerSingleton.previous() + } + + KeyEvent.KEYCODE_MEDIA_STOP -> { + PlayerSingleton.stop() + } } + } else if (intent.hasExtra(PlayerPlugin.FAVORITE)) { + PlayerSingleton.favorite(intent.getBooleanExtra(PlayerPlugin.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..79ae3e3a 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,10 +1,7 @@ package br.com.suamusica.player -import android.annotation.SuppressLint -import android.app.Notification import android.app.PendingIntent import android.app.Service -import android.content.ComponentName import android.content.Context import android.content.Intent import android.graphics.Bitmap @@ -15,44 +12,40 @@ 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.ForwardingPlayer +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.MediaMetadata.PICTURE_TYPE_FRONT_COVER +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.Timeline +import androidx.media3.common.Tracks +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.ProgressiveMediaSource +import androidx.media3.session.CacheBitmapLoader +import androidx.media3.session.CommandButton +import androidx.media3.session.DefaultMediaNotificationProvider +import androidx.media3.session.MediaController +import androidx.media3.session.MediaNotification +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSessionService 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 kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.ListenableFuture +import java.io.ByteArrayOutputStream import java.io.File import java.io.BufferedReader import java.io.InputStreamReader @@ -60,28 +53,22 @@ import java.io.IOException import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean -class MediaService : androidx.media.MediaBrowserServiceCompat() { +@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 + var mediaSession: MediaSession? = null + private var mediaController: ListenableFuture? = null - private val uAmpAudioAttributes = AudioAttributes.Builder() - .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) - .setUsage(C.USAGE_MEDIA) - .build() + private val uAmpAudioAttributes = + AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA) + .build() private var player: ExoPlayer? = null @@ -89,143 +76,105 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { private var previousState: Int = -1 - private val BROWSABLE_ROOT = "/" - private val EMPTY_ROOT = "@empty@" - - private val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true) - private val isHyperOS = !getProperty("ro.mi.os.version.name").isNullOrBlank() - - companion object { - private val glideOptions = RequestOptions() - .fallback(R.drawable.default_art) - .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) - .timeout(5000) - - 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) - - file.exists() -> glider.load(Uri.fromFile(file)) - .submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE) - - else -> null - } - - 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 - } - } - } - withContext(Dispatchers.Main) { - callback(bitmap) - } - } - } - } + private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader + private lateinit var mediaButtonEventHandler: MediaButtonEventHandler 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() + mediaButtonEventHandler = MediaButtonEventHandler(this) 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( + 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(), - ) - } + + dataSourceBitmapLoader = + DataSourceBitmapLoader(applicationContext) + Log.d(TAG, "MEDIA3 - handleCustomCommand ${Build.VERSION_CODES.TIRAMISU}") + player?.let { + mediaSession = MediaSession.Builder(this, it) + .setBitmapLoader(CacheBitmapLoader(dataSourceBitmapLoader)) + .setCallback(mediaButtonEventHandler) + .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) + } + + val customMedia3Notification = + defaultMediaNotificationProvider.createNotification( + mediaSession, + mediaSession.customLayout, + actionFactory, + onNotificationChangedCallback, + ) + + 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 } - 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 + val NOW_PLAYING_NOTIFICATION = 0xb339 + val notifyPendingIntent = 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 ) + return MediaNotification( + NOW_PLAYING_NOTIFICATION, + customMedia3Notification.notification.apply { + contentIntent = notifyPendingIntent + }) } - } + + override fun handleCustomCommand( + session: MediaSession, + action: String, + extras: Bundle + ): Boolean { + Log.d(TAG, "#MEDIA3# - handleCustomCommand $action") + return false + } + }) } } - override fun onTaskRemoved(rootIntent: Intent) { + override fun onGetSession( + controllerInfo: MediaSession.ControllerInfo + ): MediaSession? = mediaSession + + override fun onTaskRemoved(rootIntent: Intent?) { Log.d(TAG, "onTaskRemoved") super.onTaskRemoved(rootIntent) @@ -243,16 +192,13 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { override fun onDestroy() { removeNotification() Log.d(TAG, "onDestroy") -// mediaController?.unregisterCallback(mediaControllerCallback) releaseLock() - mediaSessionConnector?.setPlayer(null) player?.release() stopSelf() mediaSession?.run { - isActive = false - release() - Log.d("MusicService", "onDestroy(isActive: $isActive)") + releaseAndPerformAndDisableTracking() + Log.d("MusicService", "onDestroy") } releasePossibleLeaks() @@ -262,12 +208,9 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { private fun releasePossibleLeaks() { player?.release() - notificationManager = null - notificationBuilder = null packageValidator = null mediaSession = null mediaController = null - mediaSessionConnector = null wifiLock = null wakeLock = null @@ -287,91 +230,28 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { } } - override fun onGetRoot( - clientPackageName: String, - clientUid: Int, - rootHints: Bundle? - ): BrowserRoot? { - val isKnowCaller = packageValidator?.isKnownCaller(clientPackageName, clientUid) ?: false - - return if (isKnowCaller) { - BrowserRoot(BROWSABLE_ROOT, null) - } else { - BrowserRoot(EMPTY_ROOT, null) - } - } - - override fun onLoadChildren( - parentId: String, - result: Result> - ) { - result.sendResult(mutableListOf()) - } - fun prepare(cookie: String, media: Media) { this.media = media - 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 = 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 - } - - 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() - } - } - } - mediaSessionConnector?.setQueueNavigator(timelineQueueNavigator) - } + val metadata = buildMetaData(media) val url = media.url Log.i(TAG, "Player: URL: $url") val uri = if (url.startsWith("/")) Uri.fromFile(File(url)) else Uri.parse(url) - + val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata).build() @C.ContentType val type = Util.inferContentType(uri) - Log.i(TAG, "Player: Type: $type HLS: ${C.TYPE_HLS}") + Log.i(TAG, "Player: Type: $type HLS: ${C.CONTENT_TYPE_HLS}") val source = when (type) { - C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory) + C.CONTENT_TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory) .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) - .setAllowChunklessPreparation(true) - .createMediaSource(MediaItem.fromUri(uri)) - C.TYPE_OTHER -> { + .setAllowChunklessPreparation(true).createMediaSource(mediaItem) + + C.CONTENT_TYPE_OTHER -> { Log.i(TAG, "Player: URI: $uri") val factory: DataSource.Factory = if (uri.scheme != null && uri.scheme?.startsWith("http") == true) { @@ -380,8 +260,9 @@ 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") } @@ -390,62 +271,49 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { player?.prepare(source) } - fun play() { - performAndEnableTracking { - player?.play() - } - } - 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) - } + private fun buildMetaData(media: Media): MediaMetadata { + val metadataBuilder = MediaMetadata.Builder() + + if (media.isFavorite != null) { + mediaSession?.sessionExtras?.putBoolean( + PlayerPlugin.IS_FAVORITE_ARGUMENT, + media.isFavorite + ) } - } - fun sendCommand(type: String) { - val extra = Bundle() - extra.putString("type", type) - mediaSession?.setExtras(extra) + val stream = ByteArrayOutputStream() - } + val art = try { + dataSourceBitmapLoader.loadBitmap(Uri.parse(media.bigCoverUrl!!)) + .get(5000, TimeUnit.MILLISECONDS) + } catch (e: Exception) { + BitmapFactory.decodeResource(resources, R.drawable.default_art) + } - fun setFavorite(favorite: Boolean?) { - media?.let { - this.media = Media(it.name, it.author, it.url, it.coverUrl, it.bigCoverUrl, favorite) - sendNotification(this.media!!, null) + art?.compress(Bitmap.CompressFormat.PNG, 95, stream) + metadataBuilder.apply { + setAlbumTitle(media.name) + setArtist(media.author) + setArtworkData(stream.toByteArray(), PICTURE_TYPE_FRONT_COVER) + setArtist(media.author) + setTitle(media.name) + setDisplayTitle(media.name) } + val metadata = metadataBuilder.build() + return metadata } - 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 - } - this.media = media - val notification = notificationBuilder?.buildNotification( - it, - media, - onGoing, - isPlayingExternal, - media.isFavorite, - player?.duration, bitmap - ) - notification?.let { - notificationManager?.notify(NOW_PLAYING_NOTIFICATION, notification) - } - } + fun play() { + performAndEnableTracking { + player?.play() } } - + fun removeNotification() { - removeNowPlayingNotification(); + object: ForwardingPlayer(player!!) { + override fun getDuration(): Long { + return C.TIME_UNSET + } + } } fun seek(position: Long, playWhenReady: Boolean) { @@ -475,20 +343,12 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { } } - 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 @@ -500,7 +360,7 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { extra.putString("type", "position") extra.putLong("position", position) extra.putLong("duration", duration) - mediaSession?.setExtras(extra) + mediaSession?.setSessionExtras(extra) } } @@ -535,28 +395,6 @@ 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 - ) - } - } else { - null - } - } - private fun playerEventListener(): Player.Listener { return object : Player.Listener { override fun onTimelineChanged(timeline: Timeline, reason: Int) { @@ -567,16 +405,22 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { Log.i(TAG, "onTracksChanged: ") } - override fun onLoadingChanged(isLoading: Boolean) { - Log.i(TAG, "onLoadingChanged: isLoading: $isLoading") + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + Log.i(TAG, "onPositionDiscontinuity: $reason") + if (reason == DISCONTINUITY_REASON_SEEK) { + val bundle = Bundle() + bundle.putString("type", "seek-end") + mediaSession?.setSessionExtras(bundle) + } } - override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { - Log.i( - TAG, - "onPlayerStateChanged: playWhenReady: $playWhenReady playbackState: $playbackState currentPlaybackState: ${player?.playbackState}" - ) - if (playWhenReady) { + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + if (isPlaying) { val duration = player?.duration ?: 0L acquireLock( if (duration > 1L) duration + TimeUnit.MINUTES.toMillis(2) else TimeUnit.MINUTES.toMillis( @@ -585,47 +429,22 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { ) } else releaseLock() + } - if (playWhenReady && playbackState == ExoPlayer.STATE_READY) { - // - } else { - if (player?.playerError != null) { - // + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + if (playbackState == Player.STATE_READY) { + 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 { - 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 { - // - } - } - } + stopTrackingProgressAndPerformTask {} } + } else if (playbackState == Player.STATE_ENDED) { + stopTrackingProgressAndPerformTask {} } previousState = playbackState } @@ -648,23 +467,12 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { .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) - } - + mediaSession?.setSessionExtras(bundle) } override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { Log.i(TAG, "onPlaybackParametersChanged: $playbackParameters") } - } } @@ -698,28 +506,7 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { } } - 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() { + private fun stopService() { if (isForegroundService) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { stopForeground(STOP_FOREGROUND_DETACH) @@ -731,81 +518,4 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { 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..fea0cc70 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 @@ -51,7 +51,6 @@ class MediaSessionConnection( 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) } @@ -76,7 +75,7 @@ class MediaSessionConnection( fun favorite(shouldFavorite:Boolean) { val bundle = Bundle() bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, shouldFavorite) - sendCommand(FAVORITE, bundle) + sendCommand(PlayerPlugin.FAVORITE, bundle) } fun stop() { @@ -101,10 +100,6 @@ class MediaSessionConnection( 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) } @@ -165,7 +160,7 @@ class MediaSessionConnection( var lastState = PlaybackStateCompat.STATE_NONE - 1 override fun onPlaybackStateChanged(state: PlaybackStateCompat) { if (lastState != state.state) { - Log.i(TAG, "onPlaybackStateChanged: $state") + Log.i(TAG, "onPlaybackStateChanged2: $state") lastState = state.state playerChangeNotifier.notifyStateChange(state.state) } @@ -200,7 +195,7 @@ class MediaSessionConnection( } override fun onMetadataChanged(metadata: MediaMetadataCompat) { - Log.i(TAG, "onMetadataChanged: $metadata duration: ${metadata.duration}") +// Log.i(TAG, "onMetadataChanged: $metadata duration: ${metadata.duration}") } }) } 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/PlayerPlugin.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt index 913b73fb..7590e759 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 @@ -24,6 +24,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { 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" @@ -94,6 +95,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { val cookie = call.argument("cookie") PlayerSingleton.externalPlayback = call.argument("externalplayback") Log.d(TAG, "method: ${call.method} cookie: $cookie externalPlayback: ${PlayerSingleton.externalPlayback}") + Log.d(TAG, "TESTE1 method: ${call.method} | ${call.argument(IS_FAVORITE_ARGUMENT)}") when (call.method) { LOAD_METHOD -> { val name = call.argument(NAME_ARGUMENT)!! 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..8170ecdc 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,7 +8,6 @@ object PlayerSingleton { var channel: MethodChannel? = null var mediaSessionConnection: MediaSessionConnection? = null var externalPlayback: Boolean? = false - var lastFavorite: Boolean=false private const val TAG = "Player" fun setChannel(c: MethodChannel, context: Context) { @@ -27,14 +26,14 @@ object PlayerSingleton { channel?.invokeMethod("commandCenter.onPlay", emptyMap()) } } - - fun togglePlayPause(){ - mediaSessionConnection?.togglePlayPause() - channel?.invokeMethod("commandCenter.onTogglePlayPause", emptyMap()) - } - fun adsPlaying(){ - mediaSessionConnection?.adsPlaying() - } +//TODO(Lucas) verificar se pode retirar +// fun togglePlayPause(){ +// mediaSessionConnection?.togglePlayPause() +// channel?.invokeMethod("commandCenter.onTogglePlayPause", emptyMap()) +// } +// fun adsPlaying(){ +// mediaSessionConnection?.adsPlaying() +// } fun pause() { if (externalPlayback!!) { channel?.invokeMethod("externalPlayback.pause", emptyMap()) @@ -49,6 +48,7 @@ object PlayerSingleton { } fun next() { + Log.d("Player", "#MEDIA3# - commandCenter NEXT") channel?.invokeMethod("commandCenter.onNext", emptyMap()) } @@ -58,10 +58,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/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/example/.flutter-plugins-dependencies b/packages/player/example/.flutter-plugins-dependencies index 069a4193..acf34efb 100644 --- a/packages/player/example/.flutter-plugins-dependencies +++ b/packages/player/example/.flutter-plugins-dependencies @@ -1 +1,134 @@ -{"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/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/Documents/Dev/SM/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.4/", + "native_build": true, + "dependencies": [] + }, + { + "name": "smplayer", + "path": "/Users/suamusica/Documents/Dev/SM/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-05-09 11:03:41.030919", + "version": "3.19.1" +} \ 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">