From c0313818bc725f990cb10120215f491856c19ead Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 9 May 2024 11:48:00 -0300 Subject: [PATCH 01/70] wip --- packages/player/android/build.gradle | 13 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../android/src/main/AndroidManifest.xml | 20 +- .../player/FavoriteModeActionProvider.kt | 56 +- .../player/MediaButtonEventHandler.kt | 195 +- .../player/MediaControlBroadcastReceiver.kt | 2 +- .../br/com/suamusica/player/MediaService.kt | 1037 +++++---- .../player/MusicPlayerPlaybackPreparer.kt | 238 +- .../suamusica/player/NextActionProvider.kt | 38 +- .../suamusica/player/NotificationBuilder.kt | 51 +- .../com/suamusica/player/PackageValidator.kt | 2 +- .../player/PreviousActionProvider.kt | 38 +- .../media/parser/CustomHlsMasterPlaylist.java | 644 +++--- .../media/parser/CustomHlsPlaylistParser.java | 1960 ++++++++--------- .../parser/SMHlsPlaylistParserFactory.java | 90 +- .../example/.flutter-plugins-dependencies | 2 +- .../player/example/android/app/build.gradle | 23 +- .../android/app/src/main/AndroidManifest.xml | 7 +- packages/player/example/android/build.gradle | 26 +- .../player/example/android/gradle.properties | 6 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 21 files changed, 2344 insertions(+), 2108 deletions(-) diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index 7505b818..04d7c692 100644 --- a/packages/player/android/build.gradle +++ b/packages/player/android/build.gradle @@ -11,7 +11,7 @@ buildscript { } 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,23 @@ 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:1.2.1" 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:1.2.1' + implementation 'androidx.media3:media3-session:1.2.1' + implementation "androidx.media3:media3-common:1.2.1" +// 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..21ace208 100644 --- a/packages/player/android/src/main/AndroidManifest.xml +++ b/packages/player/android/src/main/AndroidManifest.xml @@ -1,22 +1,24 @@ + 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 index 31bfcd74..1c4b42ba 100644 --- 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 @@ -1,28 +1,28 @@ -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 +//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 androidx.media3.common.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..98c3366c 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,18 +1,140 @@ package br.com.suamusica.player import android.content.Intent +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.util.UnstableApi +import androidx.media3.session.MediaSession +import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionResult +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, +) : MediaSession.Callback { + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): MediaSession.ConnectionResult { + Log.d("Player", "onConnect") + val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() + .add(SessionCommand("NEXT", Bundle.EMPTY)) + .add(SessionCommand("seek", session.token.extras)) + .add(SessionCommand("PREVIOUS", Bundle.EMPTY)) + .add(SessionCommand("Favoritar", Bundle.EMPTY)) + .add(SessionCommand("prepare", session.token.extras)) + .add(SessionCommand("play", Bundle.EMPTY)) + .add(SessionCommand("send_notification", session.token.extras)) + .build() + return MediaSession.ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands(sessionCommands) + .build() + } + + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: MediaSession.ControllerInfo, + mediaItems: MutableList + ): ListenableFuture> { + Log.d("Player", "onAddMediaItems") + return super.onAddMediaItems(mediaSession, controller, mediaItems) + } + + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + if (customCommand.customAction == "send_notification") { + Log.d("Player", "send_notification2") + args.let { + val name = it.getString(PlayerPlugin.NAME_ARGUMENT)!! + val author = it.getString(PlayerPlugin.AUTHOR_ARGUMENT)!! + val url = it.getString(PlayerPlugin.URL_ARGUMENT)!! + val coverUrl = it.getString(PlayerPlugin.COVER_URL_ARGUMENT)!! + val isPlaying = it.getBoolean(PlayerPlugin.IS_PLAYING_ARGUMENT) + val isFavorite = it.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT) + val bigCoverUrl = it.getString(PlayerPlugin.BIG_COVER_URL_ARGUMENT) + mediaService.sendNotification( + Media( + name, + author, + url, + coverUrl, + bigCoverUrl, + isFavorite, + ), + isPlaying, + ) + } + } + if (customCommand.customAction == "play") { + mediaService.play() + } + if (customCommand.customAction == "NEXT") { + PlayerSingleton.next() + } + if (customCommand.customAction == "seek") { + mediaService.seek(args.getLong("position"), args.getBoolean("playWhenReady")) + } + if (customCommand.customAction == "Favoritar") { + // Do custom logic here +// saveToFavorites(session.player.currentMediaItem) + PlayerSingleton.favorite( + session.player.currentMediaItem?.mediaMetadata?.extras?.getBoolean( + FAVORITE, + false + ) ?: false + ) - override fun onMediaButtonEvent(player: Player, intent: Intent): Boolean { +// mediaService.setFavorite(it.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT)) + + return Futures.immediateFuture( + SessionResult(SessionResult.RESULT_SUCCESS) + ) + } + if (customCommand.customAction == "prepare") { + Log.d("Player", "prepare2") + 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) + ) + } + } + return Futures.immediateFuture( + SessionResult(SessionResult.RESULT_SUCCESS) + ) + } + + + @UnstableApi + override fun onMediaButtonEvent( + session: MediaSession, + controllerInfo: MediaSession.ControllerInfo, + intent: Intent + ): Boolean { onMediaButtonEventHandler(intent) return true } + @UnstableApi fun onMediaButtonEventHandler(intent: Intent?) { if (intent == null) { @@ -20,43 +142,44 @@ class MediaButtonEventHandler : MediaSessionConnector.MediaButtonEventHandler { } if (Intent.ACTION_MEDIA_BUTTON == intent.action) { - mediaButtonHandler(intent) - } else if (intent.hasExtra(FAVORITE)) { - PlayerSingleton.favorite(intent.getBooleanExtra(FAVORITE, false)) - } + val ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) + Log.d("Player", "Key: $ke") - } + if (ke!!.action == KeyEvent.ACTION_UP) { + return + } - private fun mediaButtonHandler(intent: Intent) { - val ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) - Log.d("Player", "Key: $ke") + when (ke.keyCode) { + KeyEvent.KEYCODE_MEDIA_PLAY -> { + mediaService.play() + } - if (ke!!.action == KeyEvent.ACTION_UP) { - return - } + KeyEvent.KEYCODE_MEDIA_PAUSE -> { + mediaService.pause() + } - when (ke.keyCode) { - KeyEvent.KEYCODE_MEDIA_PLAY -> { - PlayerSingleton.play() - } - KeyEvent.KEYCODE_MEDIA_PAUSE -> { - PlayerSingleton.pause() - } - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { - Log.d("Player", "Player: Key Code : PlayPause") - PlayerSingleton.togglePlayPause() - } - KeyEvent.KEYCODE_MEDIA_NEXT -> { - Log.d("Player", "Player: Key Code : Next") - PlayerSingleton.next() - } - KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { - Log.d("Player", "Player: Key Code : Previous") - PlayerSingleton.previous() - } - KeyEvent.KEYCODE_MEDIA_STOP -> { - PlayerSingleton.stop() + 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(FAVORITE)) { + PlayerSingleton.favorite(intent.getBooleanExtra(FAVORITE, false)) } + } } \ 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 index 3a95336e..5ff206e5 100644 --- 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 @@ -6,6 +6,6 @@ import android.content.Intent class MediaControlBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - MediaButtonEventHandler().onMediaButtonEventHandler(intent) + // MediaButtonEventHandler().onMediaButtonEventHandler(intent) } } \ 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 abb3a818..fbc2b8a9 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 @@ -22,32 +22,41 @@ 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.annotation.OptIn import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.media.session.MediaButtonReceiver -import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory +import androidx.media3.common.C +import androidx.media3.common.MediaItem +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.AudioAttributes +import androidx.media3.common.MediaMetadata +import androidx.media3.common.MediaMetadata.PICTURE_TYPE_FRONT_COVER +import androidx.media3.common.util.UnstableApi +import androidx.media3.common.util.Util +import androidx.media3.datasource.DataSource +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.CommandButton +import androidx.media3.session.MediaController +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSessionService +import androidx.media3.session.SessionCommand +//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 com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -57,23 +66,20 @@ import java.io.File 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 + private var mediaSession: MediaSession? = null + private var mediaController: ListenableFuture? = null private val uAmpAudioAttributes = AudioAttributes.Builder() .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) @@ -88,6 +94,7 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { private val BROWSABLE_ROOT = "/" private val EMPTY_ROOT = "@empty@" + companion object { private val glideOptions = RequestOptions() .fallback(R.drawable.default_art) @@ -97,7 +104,7 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { private const val NOTIFICATION_LARGE_ICON_SIZE = 500 // px private const val LOCAL_COVER_PNG = "../app_flutter/covers/0.png" // px - @OptIn(DelicateCoroutinesApi::class) + @kotlin.OptIn(DelicateCoroutinesApi::class) fun getArts(context: Context, artUri: String?, callback: (Bitmap?) -> Unit) { GlobalScope.launch(Dispatchers.IO) { Log.i("getArts", " artUri: $artUri") @@ -135,11 +142,11 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { } } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d(TAG, "onStartCommand") - return Service.START_STICKY - - } +// override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { +// Log.d(TAG, "onStartCommand") +// return Service.START_STICKY +// +// } override fun onCreate() { super.onCreate() @@ -156,20 +163,20 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { 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) +// 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) @@ -177,46 +184,77 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { // 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 (Build.MANUFACTURER.equals("samsung", ignoreCase = true)) { - connector.setCustomActionProviders( - FavoriteModeActionProvider(applicationContext), - NextActionProvider(), - PreviousActionProvider(), - ) - } else { - connector.setCustomActionProviders( - FavoriteModeActionProvider(applicationContext), - PreviousActionProvider(), - NextActionProvider(), - ) - } - } - connector.setMediaButtonEventHandler(MediaButtonEventHandler()) - connector.setEnabledPlaybackActions( - PlaybackStateCompat.ACTION_PLAY - or PlaybackStateCompat.ACTION_PAUSE - or PlaybackStateCompat.ACTION_REWIND - or PlaybackStateCompat.ACTION_FAST_FORWARD - or PlaybackStateCompat.ACTION_SEEK_TO - ) - } - } - } - } + + + + player?.let { + mediaSession = MediaSession.Builder(this, it) +// .setCustomLayout( +// ImmutableList.of( +// CommandButton.Builder() +// .setDisplayName("Save to favorites") +// .setIconResId(R.drawable.ic_favorite_notification_player) +// .setSessionCommand(SessionCommand("Favoritar", Bundle())) +// .build(), +// CommandButton.Builder() +// .setDisplayName("PREVIOUS") +// .setIconResId(R.drawable.ic_prev_notification_player) +// .setSessionCommand(SessionCommand("NEXT", Bundle())) +// .build(), +// CommandButton.Builder() +// .setDisplayName("NEXT") +// .setIconResId(R.drawable.ic_next_notification_player) +// .setSessionCommand(SessionCommand("NEXT", Bundle())) +// .build() +// ) +// ) + .setCallback(MediaButtonEventHandler(this)).build() + } + +// 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 (Build.MANUFACTURER.equals("samsung", ignoreCase = true)) { +// connector.setCustomActionProviders( +// FavoriteModeActionProvider(applicationContext), +// NextActionProvider(), +// PreviousActionProvider(), +// ) +// } else { +// connector.setCustomActionProviders( +// FavoriteModeActionProvider(applicationContext), +// PreviousActionProvider(), +// NextActionProvider(), +// ) +// } +// } +// connector.setMediaButtonEventHandler(MediaButtonEventHandler()) +// connector.setEnabledPlaybackActions( +// PlaybackStateCompat.ACTION_PLAY +// or PlaybackStateCompat.ACTION_PAUSE +// or PlaybackStateCompat.ACTION_REWIND +// or PlaybackStateCompat.ACTION_FAST_FORWARD +// or PlaybackStateCompat.ACTION_SEEK_TO +// ) +// } +// } +// } + } + + override fun onGetSession( + controllerInfo: MediaSession.ControllerInfo + ): MediaSession? = mediaSession override fun onTaskRemoved(rootIntent: Intent) { Log.d(TAG, "onTaskRemoved") @@ -238,14 +276,14 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { Log.d(TAG, "onDestroy") // mediaController?.unregisterCallback(mediaControllerCallback) releaseLock() - mediaSessionConnector?.setPlayer(null) +// mediaSessionConnector?.setPlayer(null) player?.release() stopSelf() mediaSession?.run { - isActive = false + player.release() release() - Log.d("MusicService", "onDestroy(isActive: $isActive)") + mediaSession = null } releasePossibleLeaks() @@ -260,7 +298,7 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { packageValidator = null mediaSession = null mediaController = null - mediaSessionConnector = null +// mediaSessionConnector = null wifiLock = null wakeLock = null @@ -280,26 +318,26 @@ 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()) - } +// 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 @@ -312,481 +350,510 @@ class MediaService : androidx.media.MediaBrowserServiceCompat() { dataSourceFactory.setDefaultRequestProperties(mapOf("Cookie" to cookie)) // Metadata Build - val metadataBuilder = MediaMetadataCompat.Builder() + val metadataBuilder = MediaMetadata.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) +// } 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) + setAlbumTitle(media.name) + setArtist(media.author) +// setArtworkData(art,PICTURE_TYPE_FRONT_COVER) + setArtist(media.author) + setTitle(media.name) + setDisplayTitle(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 = metadataBuilder.build() - mediaSession?.setMetadata(metadata) - mediaSessionConnector?.setMediaMetadataProvider { - return@setMediaMetadataProvider metadata - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - val timelineQueueNavigator = object : TimelineQueueNavigator(mediaSession!!) { - override fun getSupportedQueueNavigatorActions(player: Player): Long { - return PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT or - PlaybackStateCompat.ACTION_SEEK_TO + val 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}") + val source = when (type) { +// C.CONTENT_TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory) +// .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) +// .setAllowChunklessPreparation(true) +// .createMediaSource(MediaItem.fromUri(uri)) + + C.CONTENT_TYPE_OTHER -> { + Log.i(TAG, "Player: URI: $uri") + val factory: DataSource.Factory = + if (uri.scheme != null && uri.scheme?.startsWith("http") == true) { + dataSourceFactory + } else { + FileDataSource.Factory() + } + + ProgressiveMediaSource.Factory(factory) + .createMediaSource(MediaItem.fromUri(uri)) } - override fun getMediaDescription( - player: Player, - windowIndex: Int - ): MediaDescriptionCompat { - player.let { - return MediaDescriptionCompat.Builder().apply { - setTitle(media.author) - setSubtitle(media.name) - setIconUri(Uri.parse(media.coverUrl)) - }.build() - } + else -> { + throw IllegalStateException("Unsupported type: $type") } } - mediaSessionConnector?.setQueueNavigator(timelineQueueNavigator) + player?.pause() + player?.prepare(source) } - val url = media.url - Log.i(TAG, "Player: URL: $url") - - val uri = if (url.startsWith("/")) Uri.fromFile(File(url)) else Uri.parse(url) - - @C.ContentType val type = Util.inferContentType(uri) - Log.i(TAG, "Player: Type: $type HLS: ${C.TYPE_HLS}") - val source = when (type) { - C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory) - .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) - .setAllowChunklessPreparation(true) - .createMediaSource(MediaItem.fromUri(uri)) - C.TYPE_OTHER -> { - Log.i(TAG, "Player: URI: $uri") - val factory: DataSource.Factory = - if (uri.scheme != null && uri.scheme?.startsWith("http") == true) { - dataSourceFactory - } else { - FileDataSource.Factory() - } - ProgressiveMediaSource.Factory(factory).createMediaSource(MediaItem.fromUri(uri)) - } - else -> { - throw IllegalStateException("Unsupported type: $type") +// } + fun play() { + performAndEnableTracking { + player?.play() } } - player?.pause() - 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) + fun adsPlaying() { + getArts(applicationContext, null) { bitmap -> + this.media = Media("Propaganda", "", "", "", null, null) + val notification = + buildNotification(PlaybackStateCompat.STATE_PLAYING, true, bitmap) + notification?.let { + notificationManager?.notify(NOW_PLAYING_NOTIFICATION, it) + shouldStartService(it) + } } } - } - fun sendCommand(type: String) { - val extra = Bundle() - extra.putString("type", type) - mediaSession?.setExtras(extra) - } + fun sendCommand(type: String) { + val extra = Bundle() + extra.putString("type", type) + mediaSession?.setSessionExtras(extra) + } - fun setFavorite(favorite: Boolean?) { - media?.let { - this.media = Media(it.name, it.author, it.url, it.coverUrl, it.bigCoverUrl, favorite) - sendNotification(this.media!!, null) + fun setFavorite(favorite: Boolean?) { + media?.let { + this.media = + Media(it.name, it.author, it.url, it.coverUrl, it.bigCoverUrl, favorite) + sendNotification(this.media!!, null) + } } - } - 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 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 removeNotification() { - removeNowPlayingNotification(); - } + fun removeNotification() { + removeNowPlayingNotification(); + } - fun seek(position: Long, playWhenReady: Boolean) { - player?.seekTo(position) - player?.playWhenReady = playWhenReady - } + fun seek(position: Long, playWhenReady: Boolean) { + player?.seekTo(position) + player?.playWhenReady = playWhenReady + } - fun pause() { - performAndDisableTracking { - player?.pause() + fun pause() { + performAndDisableTracking { + player?.pause() + } } - } - fun stop() { - performAndDisableTracking { - player?.stop() + fun stop() { + performAndDisableTracking { + player?.stop() + } } - } - fun togglePlayPause() { - performAndDisableTracking { - if (player?.isPlaying == true) { - player?.pause() - } else { - player?.play() + fun togglePlayPause() { + performAndDisableTracking { + if (player?.isPlaying == true) { + player?.pause() + } else { + player?.play() + } } } - } - fun release() { - performAndDisableTracking { - player?.stop() + fun release() { + performAndDisableTracking { + player?.stop() + } } - } - private fun removeNowPlayingNotification() { - Log.d(TAG, "removeNowPlayingNotification") - Thread(Runnable { - notificationManager?.cancel(NOW_PLAYING_NOTIFICATION) - }).start() + private fun removeNowPlayingNotification() { + Log.d(TAG, "removeNowPlayingNotification") + Thread(Runnable { + notificationManager?.cancel(NOW_PLAYING_NOTIFICATION) + }).start() - } + } - private fun notifyPositionChange() { - var position = player?.currentPosition ?: 0L - val duration = player?.duration ?: 0L - position = if (position > duration) duration else position + private fun notifyPositionChange() { + var position = player?.currentPosition ?: 0L + val duration = player?.duration ?: 0L + position = if (position > duration) duration else position - if (duration > 0) { - val extra = Bundle() - extra.putString("type", "position") - extra.putLong("position", position) - extra.putLong("duration", duration) - mediaSession?.setExtras(extra) + if (duration > 0) { + val extra = Bundle() + extra.putString("type", "position") + extra.putLong("position", position) + extra.putLong("duration", duration) + mediaSession?.setSessionExtras(extra) + } } - } - private fun startTrackingProgress() { - if (progressTracker != null) { - return + private fun startTrackingProgress() { + if (progressTracker != null) { + return + } + this.progressTracker = ProgressTracker(Handler()) } - this.progressTracker = ProgressTracker(Handler()) - } - private fun stopTrackingProgress() { - progressTracker?.stopTracking() - progressTracker = null - } - - private fun stopTrackingProgressAndPerformTask(callable: () -> Unit) { - if (progressTracker != null) { - progressTracker!!.stopTracking(callable) - } else { - callable() + private fun stopTrackingProgress() { + progressTracker?.stopTracking() + progressTracker = null } - progressTracker = null - } - private fun performAndEnableTracking(callable: () -> Unit) { - callable() - startTrackingProgress() - } + private fun stopTrackingProgressAndPerformTask(callable: () -> Unit) { + if (progressTracker != null) { + progressTracker!!.stopTracking(callable) + } else { + callable() + } + progressTracker = null + } - private fun performAndDisableTracking(callable: () -> Unit) { - callable() - stopTrackingProgress() - } + private fun performAndEnableTracking(callable: () -> Unit) { + callable() + startTrackingProgress() + } - 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 - ) + private fun performAndDisableTracking(callable: () -> Unit) { + callable() + 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 } - } else { - null } - } - private fun playerEventListener(): Player.Listener { - return object : Player.Listener { - override fun onTimelineChanged(timeline: Timeline, reason: Int) { - Log.i(TAG, "onTimelineChanged: timeline: $timeline reason: $reason") - } + private fun playerEventListener(): Player.Listener { + return object : Player.Listener { + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + Log.i(TAG, "onTimelineChanged: timeline: $timeline reason: $reason") + } - override fun onTracksChanged(tracks: Tracks) { - Log.i(TAG, "onTracksChanged: ") - } + override fun onTracksChanged(tracks: Tracks) { + Log.i(TAG, "onTracksChanged: ") + } - override fun onLoadingChanged(isLoading: Boolean) { - Log.i(TAG, "onLoadingChanged: isLoading: $isLoading") - } + override fun onLoadingChanged(isLoading: Boolean) { + Log.i(TAG, "onLoadingChanged: isLoading: $isLoading") + } - override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { - Log.i( - TAG, - "onPlayerStateChanged: playWhenReady: $playWhenReady playbackState: $playbackState currentPlaybackState: ${player?.playbackState}" - ) - if (playWhenReady) { - val duration = player?.duration ?: 0L - acquireLock( - if (duration > 1L) duration + TimeUnit.MINUTES.toMillis(2) else TimeUnit.MINUTES.toMillis( - 3 - ) + override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { + Log.i( + TAG, + "onPlayerStateChanged: playWhenReady: $playWhenReady playbackState: $playbackState currentPlaybackState: ${player?.playbackState}" ) - } else - releaseLock() + if (playWhenReady) { + val duration = player?.duration ?: 0L + acquireLock( + if (duration > 1L) duration + TimeUnit.MINUTES.toMillis(2) else TimeUnit.MINUTES.toMillis( + 3 + ) + ) + } else + releaseLock() - if (playWhenReady && playbackState == ExoPlayer.STATE_READY) { - // - } else { - if (player?.playerError != null) { + if (playWhenReady && playbackState == ExoPlayer.STATE_READY) { // } 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 { + if (player?.playerError != null) { + // + } else { + when (playbackState) { + ExoPlayer.STATE_IDLE -> { // 1 + // + } + + ExoPlayer.STATE_BUFFERING -> { // 2 + // + } + + ExoPlayer.STATE_READY -> { // 3 + val status = + if (playWhenReady) PlayerState.PLAYING else PlayerState.PAUSED + if (previousState == -1) { + // when we define that the track shall not "playWhenReady" + // no position info is sent + // therefore, we need to "emulate" the first position notification + // by sending it directly + notifyPositionChange() + } else { + if (status == PlayerState.PAUSED) { + stopTrackingProgressAndPerformTask { + // + } + } else { // } - } else { - // - } + } } - } - ExoPlayer.STATE_ENDED -> { // 4 - stopTrackingProgressAndPerformTask { - // + + ExoPlayer.STATE_ENDED -> { // 4 + stopTrackingProgressAndPerformTask { + // + } } } } } + previousState = playbackState } - previousState = playbackState - } - - override fun onRepeatModeChanged(repeatMode: Int) { - Log.i(TAG, "onRepeatModeChanged: $repeatMode") - } - override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { - Log.i(TAG, "onShuffleModeEnabledChanged: $shuffleModeEnabled") - } + override fun onRepeatModeChanged(repeatMode: Int) { + Log.i(TAG, "onRepeatModeChanged: $repeatMode") + } - override fun onPlayerError(error: PlaybackException) { - Log.e(TAG, "onPLayerError: ${error.message}", error) - val bundle = Bundle() - bundle.putString("type", "error") - bundle.putString( - "error", - if (error.cause.toString() - .contains("Permission denied") - ) "Permission denied" else error.message - ) - mediaSession?.setExtras(bundle) - } + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + Log.i(TAG, "onShuffleModeEnabledChanged: $shuffleModeEnabled") + } - override fun onPositionDiscontinuity(reason: Int) { - Log.i(TAG, "onPositionDiscontinuity: $reason") - if (reason == DISCONTINUITY_REASON_SEEK) { + override fun onPlayerError(error: PlaybackException) { + Log.e(TAG, "onPLayerError: ${error.message}", error) val bundle = Bundle() - bundle.putString("type", "seek-end") - mediaSession?.setExtras(bundle) + bundle.putString("type", "error") + bundle.putString( + "error", + if (error.cause.toString() + .contains("Permission denied") + ) "Permission denied" else error.message + ) + mediaSession?.setSessionExtras(bundle) } - } - - override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { - Log.i(TAG, "onPlaybackParametersChanged: $playbackParameters") - } + override fun onPositionDiscontinuity(reason: Int) { + Log.i(TAG, "onPositionDiscontinuity: $reason") + if (reason == DISCONTINUITY_REASON_SEEK) { + val bundle = Bundle() + bundle.putString("type", "seek-end") + mediaSession?.setSessionExtras(bundle) + } - } - } + } - private inner class ProgressTracker(val handler: Handler) : Runnable { - private val shutdownRequest = AtomicBoolean(false) - private var shutdownTask: (() -> Unit)? = null + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { + Log.i(TAG, "onPlaybackParametersChanged: $playbackParameters") + } - init { - handler.post(this) + } } - override fun run() { - notifyPositionChange() + private inner class ProgressTracker(val handler: Handler) : Runnable { + private val shutdownRequest = AtomicBoolean(false) + private var shutdownTask: (() -> Unit)? = null - if (!shutdownRequest.get()) { - handler.postDelayed(this, 800 /* ms */) - } else { - shutdownTask?.let { - it() - } + init { + handler.post(this) } - } - fun stopTracking() { - shutdownRequest.set(true) - } + override fun run() { + notifyPositionChange() - fun stopTracking(callable: () -> Unit) { - shutdownTask = callable - stopTracking() - } - } + if (!shutdownRequest.get()) { + handler.postDelayed(this, 800 /* ms */) + } else { + shutdownTask?.let { + it() + } + } + } - 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) - ) + fun stopTracking() { + shutdownRequest.set(true) } - isForegroundService = true + fun stopTracking(callable: () -> Unit) { + shutdownTask = callable + stopTracking() + } } - } - fun stopService() { - if (isForegroundService) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - stopForeground(STOP_FOREGROUND_DETACH) - } else { - stopForeground(false) + 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 + } - isForegroundService = false - stopSelf() - Log.i(TAG, "Stopping Service") } - } - private inner class MediaControllerCallback : MediaControllerCompat.Callback() { - override fun onMetadataChanged(metadata: MediaMetadataCompat?) { - Log.d( - TAG, - "onMetadataChanged: title: ${metadata?.title} duration: ${metadata?.duration}" - ) + fun stopService() { + if (isForegroundService) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + stopForeground(STOP_FOREGROUND_DETACH) + } else { + stopForeground(false) + } + isForegroundService = false + stopSelf() + Log.i(TAG, "Stopping Service") + } } - override fun onPlaybackStateChanged(state: PlaybackStateCompat?) { - Log.d(TAG, "onPlaybackStateChanged state: $state") - updateNotification(state!!) - } + private inner class MediaControllerCallback : MediaControllerCompat.Callback() { + override fun onMetadataChanged(metadata: MediaMetadataCompat?) { + Log.d( + TAG, + "onMetadataChanged: title: ${metadata?.title} duration: ${metadata?.duration}" + ) + } - override fun onQueueChanged(queue: MutableList?) { - Log.d(TAG, "onQueueChanged queue: $queue") - } + override fun onPlaybackStateChanged(state: PlaybackStateCompat?) { + Log.d(TAG, "onPlaybackStateChanged state: $state") + updateNotification(state!!) + } - @SuppressLint("WakelockTimeout") - private fun updateNotification(state: PlaybackStateCompat) { - if (mediaController?.metadata == null || mediaSession == null) { - return + override fun onQueueChanged(queue: MutableList?) { + Log.d(TAG, "onQueueChanged queue: $queue") } - 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 + + @SuppressLint("WakelockTimeout") + private fun updateNotification(state: PlaybackStateCompat) { + if (mediaSession == null) { + return } - 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) - } + 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 } - 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() - } + 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) - } else - removeNowPlayingNotification() + 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() + } } } } } } } -} 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 index abb82b12..d69a2c18 100644 --- 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 @@ -1,133 +1,133 @@ -package br.com.suamusica.player +// 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 +// import android.net.Uri +// import android.os.Bundle +// import android.os.ResultReceiver +// import android.util.Log +// import androidx.media3.common.Player +// import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -class MusicPlayerPlaybackPreparer( - private val mediaService: MediaService, - ) : MediaSessionConnector.PlaybackPreparer { - val TAG = "Player" +// 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") +// override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) { +// Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromMediaId : START") - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromMediaId : END") - } +// Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromMediaId : END") +// } - override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) { - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromSearch : START") +// override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) { +// Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromSearch : START") - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromSearch : END") - } +// Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromSearch : END") +// } - override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) { - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromUri : START") +// override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) { +// Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromUri : START") - Log.i(TAG, "MusicPlayerPlaybackPreparer.onPrepareFromUri : END") - } +// 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") +// 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") +// 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") - } - } +// 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 getSupportedPrepareActions(): Long { +// return 0L +// } - override fun onPrepare(playWhenReady: Boolean) { +// override fun onPrepare(playWhenReady: Boolean) { - } -} \ No newline at end of file +// } +// } \ 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 index 097b75ca..4d6d0721 100644 --- 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 @@ -1,23 +1,23 @@ -package br.com.suamusica.player +// 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 +// import android.os.Bundle +// import android.support.v4.media.session.PlaybackStateCompat +// import android.util.Log +// import androidx.media3.common.Player +// import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -class NextActionProvider : - MediaSessionConnector.CustomActionProvider { +// class NextActionProvider : +// MediaSessionConnector.CustomActionProvider { - override fun onCustomAction(player: Player, action: String, extras: Bundle?) { - PlayerSingleton.next() - } +// 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 +// 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 index 2b46e36a..39f09aca 100644 --- 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 @@ -9,6 +9,7 @@ import android.content.Intent import android.graphics.Bitmap import android.media.MediaMetadata import android.os.Build +import android.os.Bundle import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat.* @@ -17,8 +18,14 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.media.app.NotificationCompat.MediaStyle import androidx.media.session.MediaButtonReceiver +import androidx.media3.common.util.UnstableApi +import androidx.media3.session.CommandButton +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaStyleNotificationHelper +import androidx.media3.session.SessionCommand import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions +import com.google.common.collect.ImmutableList import kotlinx.coroutines.* import java.util.* @@ -26,7 +33,7 @@ const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 const val FAVORITE: String = "favorite" -/** +@UnstableApi /** * Helper class to encapsulate code for building notifications. */ class NotificationBuilder(private val context: Context) { @@ -84,7 +91,7 @@ class NotificationBuilder(private val context: Context) { fun buildNotification( - mediaSession: MediaSessionCompat, + mediaSession: MediaSession, media: Media?, onGoing: Boolean, isPlayingExternal: Boolean?, @@ -95,7 +102,8 @@ class NotificationBuilder(private val context: Context) { if (shouldCreateNowPlayingChannel()) { createNowPlayingChannel() } - val playbackState = mediaSession.controller.playbackState + Log.i("NotificationBuilder", "buildNotification") + val playbackState = mediaSession.player.duration val builder = NotificationCompat.Builder(context, NOW_PLAYING_CHANNEL) val actions = if (isFavorite == null) mutableListOf(0, 1, 2) else mutableListOf( 0, @@ -104,7 +112,7 @@ class NotificationBuilder(private val context: Context) { ) // favorite,play/pause,next val duration = mediaDuration ?: 0L val currentDuration = - mediaSession.controller.metadata.getLong(MediaMetadata.METADATA_KEY_DURATION) + mediaSession.player.currentPosition val shouldUseMetadata = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R isFavorite?.let { @@ -121,12 +129,14 @@ class NotificationBuilder(private val context: Context) { } } - playbackState.isPlaying -> { + //TODO: adicionar a variavel correta antes era um getter + mediaSession.player.isPlaying -> { Log.i("NotificationBuilder", "Player is playing... onGoing: $onGoing") builder.addAction(pauseAction) } - playbackState.isPlayEnabled -> { + //TODO: adicionar a variavel correta antes era um getter + mediaSession.player.isPlaying -> { Log.i("NotificationBuilder", "Player is NOT playing... onGoing: $onGoing") builder.addAction(playAction) } @@ -139,24 +149,23 @@ class NotificationBuilder(private val context: Context) { builder.addAction(skipToNextAction) - val mediaStyle = MediaStyle() + val mediaStyle = MediaStyleNotificationHelper.MediaStyle(mediaSession) .setCancelButtonIntent(stopPendingIntent) .setShowActionsInCompactView(*actions.toIntArray()) .setShowCancelButton(true) - .setMediaSession(mediaSession.sessionToken) - - if (shouldUseMetadata && 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() - ) - } + +// if (shouldUseMetadata && 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 { 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..4348534a 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 androidx.media3.common.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/PreviousActionProvider.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PreviousActionProvider.kt index dc5a58a5..b504a4c4 100644 --- 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 @@ -1,23 +1,23 @@ -package br.com.suamusica.player +// 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 +// import android.os.Bundle +// import android.support.v4.media.session.PlaybackStateCompat +// import android.util.Log +// import androidx.media3.common.Player +// import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -class PreviousActionProvider : - MediaSessionConnector.CustomActionProvider { +// class PreviousActionProvider : +// MediaSessionConnector.CustomActionProvider { - override fun onCustomAction(player: Player, action: String, extras: Bundle?) { - PlayerSingleton.previous() - } +// 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 +// 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..600c3162 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 @@ -1,322 +1,322 @@ -package br.com.suamusica.player.media.parser; - -import android.net.Uri; - -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 java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public final class CustomHlsMasterPlaylist extends HlsPlaylist { - - /** Represents an empty master playlist, from which no attributes can be inherited. */ - public static final CustomHlsMasterPlaylist EMPTY = - new CustomHlsMasterPlaylist( - /* baseUri= */ "", - /* tags= */ new ArrayList(), - /* variants= */ new ArrayList(), - /* videos= */ new ArrayList(), - /* audios= */ new ArrayList(), - /* subtitles= */ new ArrayList(), - /* closedCaptions= */ new ArrayList(), - /* muxedAudioFormat= */ null, - /* muxedCaptionFormats= */ new ArrayList(), - /* hasIndependentSegments= */ false, - /* variableDefinitions= */ new HashMap(), - /* sessionKeyDrmInitData= */ new ArrayList()); - - // These constants must not be changed because they are persisted in offline stream keys. - public static final int GROUP_INDEX_VARIANT = 0; - 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. */ - public static final class Variant { - - /** The variant's url. */ - public final Uri url; - - /** Format information associated with this variant. */ - public final Format format; - - /** 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 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; - - /** - * @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}. - */ - public Variant( - Uri url, - Format format, - @Nullable String videoGroupId, - @Nullable String audioGroupId, - @Nullable String subtitleGroupId, - @Nullable String captionGroupId) { - this.url = url; - this.format = format; - this.videoGroupId = videoGroupId; - this.audioGroupId = audioGroupId; - this.subtitleGroupId = subtitleGroupId; - this.captionGroupId = captionGroupId; - } - - /** - * Creates a variant for a given media playlist url. - * - * @param url The media playlist url. - * @return The variant instance. - */ - public static CustomHlsMasterPlaylist.Variant createMediaPlaylistVariantUrl(Uri url) { - Format format = - new Format.Builder() - .setId("0") - .setLabel(null) - .setLanguage(null) - .setSelectionFlags(0) - .setRoleFlags(0) - .setAverageBitrate(Format.NO_VALUE) - .setPeakBitrate(Format.NO_VALUE) - .setCodecs(null) - .setContainerMimeType(MimeTypes.APPLICATION_M3U8) - .setSampleMimeType(null) - .build(); - return new CustomHlsMasterPlaylist.Variant( - url, - format, - /* videoGroupId= */ null, - /* audioGroupId= */ null, - /* subtitleGroupId= */ null, - /* captionGroupId= */ null); - } - - /** 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. */ - 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; - - /** Format information associated with this rendition. */ - public final Format format; - - /** The group to which this rendition belongs. */ - public final String groupId; - - /** The name of the rendition. */ - public final String name; - - /** - * @param url See {@link #url}. - * @param format See {@link #format}. - * @param groupId See {@link #groupId}. - * @param name See {@link #name}. - */ - public Rendition(@Nullable Uri url, Format format, String groupId, String name) { - this.url = url; - this.format = format; - this.groupId = groupId; - this.name = name; - } - - } - - /** All of the media playlist URLs referenced by the playlist. */ - public final List mediaPlaylistUrls; - /** The variants declared by the playlist. */ - public final List variants; - /** The video renditions declared by the playlist. */ - public final List videos; - /** The audio renditions declared by the playlist. */ - public final List audios; - /** The subtitle renditions declared by the playlist. */ - public final List subtitles; - /** The closed caption renditions declared by the playlist. */ - public final List closedCaptions; - - /** - * The format of the audio muxed in the variants. May be null if the playlist does not declare any - * muxed audio. - */ - public final Format muxedAudioFormat; - /** - * The format of the closed captions declared by the playlist. May be empty if the playlist - * explicitly declares no captions are available, or null if the playlist does not declare any - * captions information. - */ - public final List muxedCaptionFormats; - /** 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. */ - 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 hasIndependentSegments See {@link #hasIndependentSegments}. - * @param variableDefinitions See {@link #variableDefinitions}. - * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}. - */ - public CustomHlsMasterPlaylist( - String baseUri, - List tags, - List variants, - List videos, - List audios, - List subtitles, - List closedCaptions, - Format muxedAudioFormat, - List muxedCaptionFormats, - boolean hasIndependentSegments, - Map variableDefinitions, - List sessionKeyDrmInitData) { - super(baseUri, tags, hasIndependentSegments); - this.mediaPlaylistUrls = - Collections.unmodifiableList( - getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions)); - this.variants = Collections.unmodifiableList(variants); - this.videos = Collections.unmodifiableList(videos); - this.audios = Collections.unmodifiableList(audios); - this.subtitles = Collections.unmodifiableList(subtitles); - this.closedCaptions = Collections.unmodifiableList(closedCaptions); - this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormats = muxedCaptionFormats != null - ? Collections.unmodifiableList(muxedCaptionFormats) : null; - this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions); - this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData); - } - - @Override - public CustomHlsMasterPlaylist copy(List streamKeys) { - return new CustomHlsMasterPlaylist( - baseUri, - tags, - copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys), - // TODO: Allow stream keys to specify video renditions to be retained. - /* videos= */ new ArrayList(), - copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys), - copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), - // TODO: Update to retain all closed captions. - /* closedCaptions= */ new ArrayList(), - muxedAudioFormat, - muxedCaptionFormats, - hasIndependentSegments, - variableDefinitions, - sessionKeyDrmInitData); - } - - /** - * Creates a playlist with a single variant. - * - * @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))); - return new CustomHlsMasterPlaylist( - /* baseUri= */ null, - /* tags= */ new ArrayList(), - variant, - /* videos= */ new ArrayList(), - /* audios= */ new ArrayList(), - /* subtitles= */ new ArrayList(), - /* closedCaptions= */ new ArrayList(), - /* muxedAudioFormat= */ null, - /* muxedCaptionFormats= */ null, - /* hasIndependentSegments= */ false, - /* variableDefinitions= */ new HashMap(), - /* sessionKeyDrmInitData= */ new ArrayList()); - } - - private static List getMediaPlaylistUrls( - List variants, - List videos, - List audios, - List subtitles, - List closedCaptions) { - ArrayList mediaPlaylistUrls = new ArrayList<>(); - for (int i = 0; i < variants.size(); i++) { - Uri uri = variants.get(i).url; - if (!mediaPlaylistUrls.contains(uri)) { - mediaPlaylistUrls.add(uri); - } - } - addMediaPlaylistUrls(videos, mediaPlaylistUrls); - addMediaPlaylistUrls(audios, mediaPlaylistUrls); - addMediaPlaylistUrls(subtitles, mediaPlaylistUrls); - addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls); - return mediaPlaylistUrls; - } - - private static void addMediaPlaylistUrls(List renditions, List out) { - for (int i = 0; i < renditions.size(); i++) { - Uri uri = renditions.get(i).url; - if (uri != null && !out.contains(uri)) { - out.add(uri); - } - } - } - - private static List copyStreams( - List streams, int groupIndex, List streamKeys) { - List copiedStreams = new ArrayList<>(streamKeys.size()); - // TODO: - // 1. When variants with the same URL are not de-duplicated, duplicates must not increment - // trackIndex so as to avoid breaking stream keys that have been persisted for offline. All - // duplicates should be copied if the first variant is copied, or discarded otherwise. - // 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to - // avoid breaking stream keys that have been persisted for offline. All renitions with null - // URLs should be copied. They may become unreachable if all variants that reference them are - // removed, but this is OK. - // 3. Renditions with URLs matching copied variants should always themselves be copied, even if - // the corresponding stream key is omitted. Else we're throwing away information for no gain. - for (int i = 0; i < streams.size(); i++) { - T stream = streams.get(i); - for (int j = 0; j < streamKeys.size(); j++) { - StreamKey streamKey = streamKeys.get(j); - if (streamKey.groupIndex == groupIndex && streamKey.streamIndex == i) { - copiedStreams.add(stream); - break; - } - } - } - return copiedStreams; - } - -} \ No newline at end of file +//package br.com.suamusica.player.media.parser; +// +//import android.net.Uri; +// +//import androidx.annotation.Nullable; +// +//import androidx.media3.common.Format; +//import androidx.media3.common.DrmInitData; +//import androidx.media3.common.StreamKey; +//import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; +//import androidx.media3.common.MimeTypes; +// +//import java.util.ArrayList; +//import java.util.Collections; +//import java.util.HashMap; +//import java.util.List; +//import java.util.Map; +// +//public final class CustomHlsMasterPlaylist extends HlsPlaylist { +// +// /** Represents an empty master playlist, from which no attributes can be inherited. */ +// public static final CustomHlsMasterPlaylist EMPTY = +// new CustomHlsMasterPlaylist( +// /* baseUri= */ "", +// /* tags= */ new ArrayList(), +// /* variants= */ new ArrayList(), +// /* videos= */ new ArrayList(), +// /* audios= */ new ArrayList(), +// /* subtitles= */ new ArrayList(), +// /* closedCaptions= */ new ArrayList(), +// /* muxedAudioFormat= */ null, +// /* muxedCaptionFormats= */ new ArrayList(), +// /* hasIndependentSegments= */ false, +// /* variableDefinitions= */ new HashMap(), +// /* sessionKeyDrmInitData= */ new ArrayList()); +// +// // These constants must not be changed because they are persisted in offline stream keys. +// public static final int GROUP_INDEX_VARIANT = 0; +// 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. */ +// public static final class Variant { +// +// /** The variant's url. */ +// public final Uri url; +// +// /** Format information associated with this variant. */ +// public final Format format; +// +// /** 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 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; +// +// /** +// * @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}. +// */ +// public Variant( +// Uri url, +// Format format, +// @Nullable String videoGroupId, +// @Nullable String audioGroupId, +// @Nullable String subtitleGroupId, +// @Nullable String captionGroupId) { +// this.url = url; +// this.format = format; +// this.videoGroupId = videoGroupId; +// this.audioGroupId = audioGroupId; +// this.subtitleGroupId = subtitleGroupId; +// this.captionGroupId = captionGroupId; +// } +// +// /** +// * Creates a variant for a given media playlist url. +// * +// * @param url The media playlist url. +// * @return The variant instance. +// */ +// public static CustomHlsMasterPlaylist.Variant createMediaPlaylistVariantUrl(Uri url) { +// Format format = +// new Format.Builder() +// .setId("0") +// .setLabel(null) +// .setLanguage(null) +// .setSelectionFlags(0) +// .setRoleFlags(0) +// .setAverageBitrate(Format.NO_VALUE) +// .setPeakBitrate(Format.NO_VALUE) +// .setCodecs(null) +// .setContainerMimeType(MimeTypes.APPLICATION_M3U8) +// .setSampleMimeType(null) +// .build(); +// return new CustomHlsMasterPlaylist.Variant( +// url, +// format, +// /* videoGroupId= */ null, +// /* audioGroupId= */ null, +// /* subtitleGroupId= */ null, +// /* captionGroupId= */ null); +// } +// +// /** 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. */ +// 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; +// +// /** Format information associated with this rendition. */ +// public final Format format; +// +// /** The group to which this rendition belongs. */ +// public final String groupId; +// +// /** The name of the rendition. */ +// public final String name; +// +// /** +// * @param url See {@link #url}. +// * @param format See {@link #format}. +// * @param groupId See {@link #groupId}. +// * @param name See {@link #name}. +// */ +// public Rendition(@Nullable Uri url, Format format, String groupId, String name) { +// this.url = url; +// this.format = format; +// this.groupId = groupId; +// this.name = name; +// } +// +// } +// +// /** All of the media playlist URLs referenced by the playlist. */ +// public final List mediaPlaylistUrls; +// /** The variants declared by the playlist. */ +// public final List variants; +// /** The video renditions declared by the playlist. */ +// public final List videos; +// /** The audio renditions declared by the playlist. */ +// public final List audios; +// /** The subtitle renditions declared by the playlist. */ +// public final List subtitles; +// /** The closed caption renditions declared by the playlist. */ +// public final List closedCaptions; +// +// /** +// * The format of the audio muxed in the variants. May be null if the playlist does not declare any +// * muxed audio. +// */ +// public final Format muxedAudioFormat; +// /** +// * The format of the closed captions declared by the playlist. May be empty if the playlist +// * explicitly declares no captions are available, or null if the playlist does not declare any +// * captions information. +// */ +// public final List muxedCaptionFormats; +// /** 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. */ +// 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 hasIndependentSegments See {@link #hasIndependentSegments}. +// * @param variableDefinitions See {@link #variableDefinitions}. +// * @param sessionKeyDrmInitData See {@link #sessionKeyDrmInitData}. +// */ +// public CustomHlsMasterPlaylist( +// String baseUri, +// List tags, +// List variants, +// List videos, +// List audios, +// List subtitles, +// List closedCaptions, +// Format muxedAudioFormat, +// List muxedCaptionFormats, +// boolean hasIndependentSegments, +// Map variableDefinitions, +// List sessionKeyDrmInitData) { +// super(baseUri, tags, hasIndependentSegments); +// this.mediaPlaylistUrls = +// Collections.unmodifiableList( +// getMediaPlaylistUrls(variants, videos, audios, subtitles, closedCaptions)); +// this.variants = Collections.unmodifiableList(variants); +// this.videos = Collections.unmodifiableList(videos); +// this.audios = Collections.unmodifiableList(audios); +// this.subtitles = Collections.unmodifiableList(subtitles); +// this.closedCaptions = Collections.unmodifiableList(closedCaptions); +// this.muxedAudioFormat = muxedAudioFormat; +// this.muxedCaptionFormats = muxedCaptionFormats != null +// ? Collections.unmodifiableList(muxedCaptionFormats) : null; +// this.variableDefinitions = Collections.unmodifiableMap(variableDefinitions); +// this.sessionKeyDrmInitData = Collections.unmodifiableList(sessionKeyDrmInitData); +// } +// +// @Override +// public CustomHlsMasterPlaylist copy(List streamKeys) { +// return new CustomHlsMasterPlaylist( +// baseUri, +// tags, +// copyStreams(variants, GROUP_INDEX_VARIANT, streamKeys), +// // TODO: Allow stream keys to specify video renditions to be retained. +// /* videos= */ new ArrayList(), +// copyStreams(audios, GROUP_INDEX_AUDIO, streamKeys), +// copyStreams(subtitles, GROUP_INDEX_SUBTITLE, streamKeys), +// // TODO: Update to retain all closed captions. +// /* closedCaptions= */ new ArrayList(), +// muxedAudioFormat, +// muxedCaptionFormats, +// hasIndependentSegments, +// variableDefinitions, +// sessionKeyDrmInitData); +// } +// +// /** +// * Creates a playlist with a single variant. +// * +// * @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))); +// return new CustomHlsMasterPlaylist( +// /* baseUri= */ null, +// /* tags= */ new ArrayList(), +// variant, +// /* videos= */ new ArrayList(), +// /* audios= */ new ArrayList(), +// /* subtitles= */ new ArrayList(), +// /* closedCaptions= */ new ArrayList(), +// /* muxedAudioFormat= */ null, +// /* muxedCaptionFormats= */ null, +// /* hasIndependentSegments= */ false, +// /* variableDefinitions= */ new HashMap(), +// /* sessionKeyDrmInitData= */ new ArrayList()); +// } +// +// private static List getMediaPlaylistUrls( +// List variants, +// List videos, +// List audios, +// List subtitles, +// List closedCaptions) { +// ArrayList mediaPlaylistUrls = new ArrayList<>(); +// for (int i = 0; i < variants.size(); i++) { +// Uri uri = variants.get(i).url; +// if (!mediaPlaylistUrls.contains(uri)) { +// mediaPlaylistUrls.add(uri); +// } +// } +// addMediaPlaylistUrls(videos, mediaPlaylistUrls); +// addMediaPlaylistUrls(audios, mediaPlaylistUrls); +// addMediaPlaylistUrls(subtitles, mediaPlaylistUrls); +// addMediaPlaylistUrls(closedCaptions, mediaPlaylistUrls); +// return mediaPlaylistUrls; +// } +// +// private static void addMediaPlaylistUrls(List renditions, List out) { +// for (int i = 0; i < renditions.size(); i++) { +// Uri uri = renditions.get(i).url; +// if (uri != null && !out.contains(uri)) { +// out.add(uri); +// } +// } +// } +// +// private static List copyStreams( +// List streams, int groupIndex, List streamKeys) { +// List copiedStreams = new ArrayList<>(streamKeys.size()); +// // TODO: +// // 1. When variants with the same URL are not de-duplicated, duplicates must not increment +// // trackIndex so as to avoid breaking stream keys that have been persisted for offline. All +// // duplicates should be copied if the first variant is copied, or discarded otherwise. +// // 2. When renditions with null URLs are permitted, they must not increment trackIndex so as to +// // avoid breaking stream keys that have been persisted for offline. All renitions with null +// // URLs should be copied. They may become unreachable if all variants that reference them are +// // removed, but this is OK. +// // 3. Renditions with URLs matching copied variants should always themselves be copied, even if +// // the corresponding stream key is omitted. Else we're throwing away information for no gain. +// for (int i = 0; i < streams.size(); i++) { +// T stream = streams.get(i); +// for (int j = 0; j < streamKeys.size(); j++) { +// StreamKey streamKey = streamKeys.get(j); +// if (streamKey.groupIndex == groupIndex && streamKey.streamIndex == i) { +// copiedStreams.add(stream); +// break; +// } +// } +// } +// return copiedStreams; +// } +// +//} \ No newline at end of file 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..f352b06e 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 @@ -1,980 +1,980 @@ -package br.com.suamusica.player.media.parser; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URLEncoder; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.TreeMap; -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 android.util.Log; - -import android.net.Uri; -import android.text.TextUtils; -import android.util.Base64; - -import androidx.annotation.Nullable; - -public class CustomHlsPlaylistParser implements ParsingLoadable.Parser { - - private static final String PLAYLIST_HEADER = "#EXTM3U"; - - private static final String TAG_PREFIX = "#EXT"; - - private static final String TAG_VERSION = "#EXT-X-VERSION"; - private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE"; - private static final String TAG_DEFINE = "#EXT-X-DEFINE"; - private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF"; - private static final String TAG_MEDIA = "#EXT-X-MEDIA"; - private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION"; - private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY"; - private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE"; - private static final String TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME"; - private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP"; - private static final String TAG_INDEPENDENT_SEGMENTS = "#EXT-X-INDEPENDENT-SEGMENTS"; - private static final String TAG_MEDIA_DURATION = "#EXTINF"; - private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE"; - private static final String TAG_START = "#EXT-X-START"; - private static final String TAG_ENDLIST = "#EXT-X-ENDLIST"; - private static final String TAG_KEY = "#EXT-X-KEY"; - private static final String TAG_SESSION_KEY = "#EXT-X-SESSION-KEY"; - private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE"; - private static final String TAG_GAP = "#EXT-X-GAP"; - - private static final String TYPE_AUDIO = "AUDIO"; - private static final String TYPE_VIDEO = "VIDEO"; - private static final String TYPE_SUBTITLES = "SUBTITLES"; - private static final String TYPE_CLOSED_CAPTIONS = "CLOSED-CAPTIONS"; - - private static final String METHOD_NONE = "NONE"; - private static final String METHOD_AES_128 = "AES-128"; - private static final String METHOD_SAMPLE_AES = "SAMPLE-AES"; - // Replaced by METHOD_SAMPLE_AES_CTR. Keep for backward compatibility. - private static final String METHOD_SAMPLE_AES_CENC = "SAMPLE-AES-CENC"; - private static final String METHOD_SAMPLE_AES_CTR = "SAMPLE-AES-CTR"; - private static final String KEYFORMAT_PLAYREADY = "com.microsoft.playready"; - private static final String KEYFORMAT_IDENTITY = "identity"; - private static final String KEYFORMAT_WIDEVINE_PSSH_BINARY = - "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"; - private static final String KEYFORMAT_WIDEVINE_PSSH_JSON = "com.widevine"; - - private static final String BOOLEAN_TRUE = "YES"; - private static final String BOOLEAN_FALSE = "NO"; - - private static final String ATTR_CLOSED_CAPTIONS_NONE = "CLOSED-CAPTIONS=NONE"; - - private static final Pattern REGEX_AVERAGE_BANDWIDTH = - Pattern.compile("AVERAGE-BANDWIDTH=(\\d+)\\b"); - private static final Pattern REGEX_VIDEO = Pattern.compile("VIDEO=\"(.+?)\""); - private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\""); - private static final Pattern REGEX_SUBTITLES = Pattern.compile("SUBTITLES=\"(.+?)\""); - private static final Pattern REGEX_CLOSED_CAPTIONS = Pattern.compile("CLOSED-CAPTIONS=\"(.+?)\""); - private static final Pattern REGEX_BANDWIDTH = Pattern.compile("[^-]BANDWIDTH=(\\d+)\\b"); - private static final Pattern REGEX_CHANNELS = Pattern.compile("CHANNELS=\"(.+?)\""); - private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\""); - private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)"); - private static final Pattern REGEX_FRAME_RATE = Pattern.compile("FRAME-RATE=([\\d\\.]+)\\b"); - private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION - + ":(\\d+)\\b"); - private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b"); - private static final Pattern REGEX_PLAYLIST_TYPE = Pattern.compile(TAG_PLAYLIST_TYPE - + ":(.+)\\b"); - private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE - + ":(\\d+)\\b"); - private static final Pattern REGEX_MEDIA_DURATION = Pattern.compile(TAG_MEDIA_DURATION - + ":([\\d\\.]+)\\b"); - private static final Pattern REGEX_MEDIA_TITLE = - Pattern.compile(TAG_MEDIA_DURATION + ":[\\d\\.]+\\b,(.+)"); - private static final Pattern REGEX_TIME_OFFSET = Pattern.compile("TIME-OFFSET=(-?[\\d\\.]+)\\b"); - private static final Pattern REGEX_BYTERANGE = Pattern.compile(TAG_BYTERANGE - + ":(\\d+(?:@\\d+)?)\\b"); - private static final Pattern REGEX_ATTR_BYTERANGE = - Pattern.compile("BYTERANGE=\"(\\d+(?:@\\d+)?)\\b\""); - private static final Pattern REGEX_METHOD = - Pattern.compile( - "METHOD=(" - + METHOD_NONE - + "|" - + METHOD_AES_128 - + "|" - + METHOD_SAMPLE_AES - + "|" - + METHOD_SAMPLE_AES_CENC - + "|" - + METHOD_SAMPLE_AES_CTR - + ")" - + "\\s*(?:,|$)"); - private static final Pattern REGEX_KEYFORMAT = Pattern.compile("KEYFORMAT=\"(.+?)\""); - private static final Pattern REGEX_KEYFORMATVERSIONS = - Pattern.compile("KEYFORMATVERSIONS=\"(.+?)\""); - private static final Pattern REGEX_URI = Pattern.compile("URI=\"(.+?)\""); - private static final Pattern REGEX_IV = Pattern.compile("IV=([^,.*]+)"); - private static final Pattern REGEX_TYPE = Pattern.compile("TYPE=(" + TYPE_AUDIO + "|" + TYPE_VIDEO - + "|" + TYPE_SUBTITLES + "|" + TYPE_CLOSED_CAPTIONS + ")"); - private static final Pattern REGEX_LANGUAGE = Pattern.compile("LANGUAGE=\"(.+?)\""); - private static final Pattern REGEX_NAME = Pattern.compile("NAME=\"(.+?)\""); - private static final Pattern REGEX_GROUP_ID = Pattern.compile("GROUP-ID=\"(.+?)\""); - private static final Pattern REGEX_CHARACTERISTICS = Pattern.compile("CHARACTERISTICS=\"(.+?)\""); - private static final Pattern REGEX_INSTREAM_ID = - Pattern.compile("INSTREAM-ID=\"((?:CC|SERVICE)\\d+)\""); - private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT"); - private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT"); - private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED"); - private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\""); - private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\""); - private static final Pattern REGEX_VARIABLE_REFERENCE = - Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}"); - - private final CustomHlsMasterPlaylist masterPlaylist; - - /** - * Creates an instance where media playlists are parsed without inheriting attributes from a - * master playlist. - */ - public CustomHlsPlaylistParser() { - this(CustomHlsMasterPlaylist.EMPTY); - } - - /** - * Creates an instance where parsed media playlists inherit attributes from the given master - * playlist. - * - * @param masterPlaylist The master playlist from which media playlists will inherit attributes. - */ - public CustomHlsPlaylistParser(CustomHlsMasterPlaylist masterPlaylist) { - this.masterPlaylist = masterPlaylist; - } - - @Override - public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { - Log.i("MusicService", "Player : Parser..."); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - Queue extraLines = new ArrayDeque<>(); - String line; - try { - if (!checkPlaylistHeader(reader)) { - throw new UnrecognizedInputFormatException("Input does not start with the #EXTM3U header.", - uri); - } - while ((line = reader.readLine()) != null) { - line = line.trim(); - Log.i("MusicService", "Player : Parser : Line 0: " + line); - if (line.isEmpty()) { - // Do nothing. - } else if (line.startsWith(TAG_STREAM_INF)) { - extraLines.add(line); - return parseMasterPlaylist(new CustomHlsPlaylistParser.LineIterator(extraLines, reader), uri.toString()); - } else if (line.startsWith(TAG_TARGET_DURATION) - || line.startsWith(TAG_MEDIA_SEQUENCE) - || line.startsWith(TAG_MEDIA_DURATION) - || line.startsWith(TAG_KEY) - || line.startsWith(TAG_BYTERANGE) - || line.equals(TAG_DISCONTINUITY) - || line.equals(TAG_DISCONTINUITY_SEQUENCE) - || line.equals(TAG_ENDLIST)) { - extraLines.add(line); - return parseMediaPlaylist( - masterPlaylist, new CustomHlsPlaylistParser.LineIterator(extraLines, reader), uri.toString()); - } else { - extraLines.add(line); - } - } - } finally { - Util.closeQuietly(reader); - } - 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) { - if (reader.read() != 0xBB || reader.read() != 0xBF) { - return false; - } - // The playlist contains a Byte Order Mark, which gets discarded. - last = reader.read(); - } - last = skipIgnorableWhitespace(reader, true, last); - int playlistHeaderLength = PLAYLIST_HEADER.length(); - for (int i = 0; i < playlistHeaderLength; i++) { - if (last != PLAYLIST_HEADER.charAt(i)) { - return false; - } - last = reader.read(); - } - last = skipIgnorableWhitespace(reader, false, last); - 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))) { - c = reader.read(); - } - return c; - } - - private static CustomHlsMasterPlaylist parseMasterPlaylist(CustomHlsPlaylistParser.LineIterator iterator, String baseUri) - throws IOException { - HashMap> urlToVariantInfos = new HashMap<>(); - HashMap variableDefinitions = new HashMap<>(); - ArrayList variants = new ArrayList<>(); - ArrayList videos = new ArrayList<>(); - ArrayList audios = new ArrayList<>(); - ArrayList subtitles = new ArrayList<>(); - ArrayList closedCaptions = new ArrayList<>(); - ArrayList mediaTags = new ArrayList<>(); - ArrayList sessionKeyDrmInitData = new ArrayList<>(); - ArrayList tags = new ArrayList<>(); - Format muxedAudioFormat = null; - List muxedCaptionFormats = null; - boolean noClosedCaptions = false; - boolean hasIndependentSegmentsTag = false; - - String line; - while (iterator.hasNext()) { - line = iterator.next(); - - Log.i("MusicService", "Player : Parser : Line 1 : " + line); - - if (line.startsWith(TAG_PREFIX)) { - // We expose all tags through the playlist. - tags.add(line); - } - - if (line.startsWith(TAG_DEFINE)) { - variableDefinitions.put( - /* key= */ parseStringAttr(line, REGEX_NAME, variableDefinitions), - /* value= */ parseStringAttr(line, REGEX_VALUE, variableDefinitions)); - } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) { - hasIndependentSegmentsTag = true; - } else if (line.startsWith(TAG_MEDIA)) { - // Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF - // tags. - mediaTags.add(line); - } else if (line.startsWith(TAG_SESSION_KEY)) { - String keyFormat = - parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions); - DrmInitData.SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions); - if (schemeData != null) { - String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions); - String scheme = parseEncryptionScheme(method); - sessionKeyDrmInitData.add(new DrmInitData(scheme, schemeData)); - } - } else if (line.startsWith(TAG_STREAM_INF)) { - noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE); - int bitrate = parseIntAttr(line, REGEX_BANDWIDTH); - String averageBandwidthString = - parseOptionalStringAttr(line, REGEX_AVERAGE_BANDWIDTH, variableDefinitions); - if (averageBandwidthString != null) { - // If available, the average bandwidth attribute is used as the variant's bitrate. - bitrate = Integer.parseInt(averageBandwidthString); - } - String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions); - String resolutionString = - parseOptionalStringAttr(line, REGEX_RESOLUTION, variableDefinitions); - int width; - int height; - if (resolutionString != null) { - String[] widthAndHeight = resolutionString.split("x"); - width = Integer.parseInt(widthAndHeight[0]); - height = Integer.parseInt(widthAndHeight[1]); - if (width <= 0 || height <= 0) { - // Resolution string is invalid. - width = Format.NO_VALUE; - height = Format.NO_VALUE; - } - } else { - width = Format.NO_VALUE; - height = Format.NO_VALUE; - } - float frameRate = Format.NO_VALUE; - String frameRateString = - parseOptionalStringAttr(line, REGEX_FRAME_RATE, variableDefinitions); - if (frameRateString != null) { - frameRate = Float.parseFloat(frameRateString); - } - String videoGroupId = parseOptionalStringAttr(line, REGEX_VIDEO, variableDefinitions); - String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO, variableDefinitions); - String subtitlesGroupId = - parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions); - String closedCaptionsGroupId = - parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions); - line = - replaceVariableReferences( - iterator.next(), variableDefinitions); // #EXT-X-STREAM-INF's URI. - Uri uri = UriUtil.resolveToUri(baseUri, line); - Format format = - new Format.Builder() - .setId(Integer.toString(variants.size())) - .setLabel(null) - .setSelectionFlags(0) - .setRoleFlags(0) - .setAverageBitrate(bitrate) - .setPeakBitrate(bitrate) - .setCodecs(codecs) - .setMetadata(null) - .setContainerMimeType(MimeTypes.APPLICATION_M3U8) - .setSampleMimeType(null) - .setInitializationData(null) - .setWidth(width) - .setHeight(height) - .setFrameRate(frameRate) - .build(); - CustomHlsMasterPlaylist.Variant variant = - new CustomHlsMasterPlaylist.Variant( - uri, format, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId); - variants.add(variant); - ArrayList variantInfosForUrl = urlToVariantInfos.get(uri); - if (variantInfosForUrl == null) { - variantInfosForUrl = new ArrayList<>(); - urlToVariantInfos.put(uri, variantInfosForUrl); - } - variantInfosForUrl.add(new HlsTrackMetadataEntry.VariantInfo(bitrate, bitrate, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId)); - } - } - - // TODO: Don't deduplicate variants by URL. - ArrayList deduplicatedVariants = new ArrayList<>(); - HashSet urlsInDeduplicatedVariants = new HashSet<>(); - for (int i = 0; i < variants.size(); i++) { - CustomHlsMasterPlaylist.Variant variant = variants.get(i); - if (urlsInDeduplicatedVariants.add(variant.url)) { - Assertions.checkState(variant.format.metadata == null); - HlsTrackMetadataEntry hlsMetadataEntry = - new HlsTrackMetadataEntry( - /* groupId= */ null, /* name= */ null, urlToVariantInfos.get(variant.url)); - deduplicatedVariants.add( - variant.copyWithFormat( - variant.format.buildUpon().setMetadata((new Metadata(hlsMetadataEntry))).build())); - } - } - - for (int i = 0; i < mediaTags.size(); i++) { - line = mediaTags.get(i); - String groupId = parseStringAttr(line, REGEX_GROUP_ID, variableDefinitions); - String name = parseStringAttr(line, REGEX_NAME, variableDefinitions); - String referenceUri = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions); - Uri uri = referenceUri == null ? null : UriUtil.resolveToUri(baseUri, referenceUri); - String language = parseOptionalStringAttr(line, REGEX_LANGUAGE, variableDefinitions); - @C.SelectionFlags int selectionFlags = parseSelectionFlags(line); - @C.RoleFlags int roleFlags = parseRoleFlags(line, variableDefinitions); - String formatId = groupId + ":" + name; - Format format; - Metadata metadata = - new Metadata(new HlsTrackMetadataEntry(groupId, name, new ArrayList())); - switch (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) { - case TYPE_VIDEO: - CustomHlsMasterPlaylist.Variant variant = getVariantWithVideoGroup(variants, groupId); - String codecs = null; - int width = Format.NO_VALUE; - int height = Format.NO_VALUE; - float frameRate = Format.NO_VALUE; - if (variant != null) { - Format variantFormat = variant.format; - codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); - width = variantFormat.width; - height = variantFormat.height; - frameRate = variantFormat.frameRate; - } - String sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null; - format = - new Format.Builder() - .setId(formatId) - .setLabel(name) - .setSelectionFlags(selectionFlags) - .setRoleFlags(roleFlags) - .setAverageBitrate(Format.NO_VALUE) - .setPeakBitrate(Format.NO_VALUE) - .setCodecs(codecs) - .setMetadata(metadata) - .setContainerMimeType(MimeTypes.APPLICATION_M3U8) - .setSampleMimeType(sampleMimeType) - .setInitializationData(null) - .setWidth(width) - .setHeight(height) - .setFrameRate(frameRate) - .build(); - //.copyWithMetadata(metadata); - if (uri == null) { - // TODO: Remove this case and add a Rendition with a null uri to videos. - } else { - videos.add(new CustomHlsMasterPlaylist.Rendition(uri, format, groupId, name)); - } - break; - case TYPE_AUDIO: - variant = getVariantWithAudioGroup(variants, groupId); - codecs = - variant != null - ? Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_AUDIO) - : null; - sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null; - int channelCount = parseChannelsAttribute(line, variableDefinitions); - format = new Format.Builder() - .setId(formatId) - .setLabel(name) - .setLanguage(language) - .setSelectionFlags(selectionFlags) - .setRoleFlags(roleFlags) - .setAverageBitrate(Format.NO_VALUE) - .setPeakBitrate(Format.NO_VALUE) - .setCodecs(codecs) - .setMetadata(metadata) - .setContainerMimeType(MimeTypes.APPLICATION_M3U8) - .setSampleMimeType(sampleMimeType) - .setInitializationData(null) - .setChannelCount(channelCount) - .setSampleRate(Format.NO_VALUE) - .build(); - if (uri == null) { - // TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios. - muxedAudioFormat = format; - } else { - audios.add(new CustomHlsMasterPlaylist.Rendition(uri, format.buildUpon().setMetadata(metadata).build(), groupId, name)); - } - break; - case TYPE_SUBTITLES: - format = - new Format.Builder() - .setId(formatId) - .setLabel(name) - .setLanguage(language) - .setSelectionFlags(selectionFlags) - .setRoleFlags(roleFlags) - .setAverageBitrate(Format.NO_VALUE) - .setPeakBitrate(Format.NO_VALUE) - .setCodecs(null) - .setContainerMimeType(MimeTypes.APPLICATION_M3U8) - .setSampleMimeType(MimeTypes.TEXT_VTT) - .setMetadata(metadata) - .build(); - subtitles.add(new CustomHlsMasterPlaylist.Rendition(uri, format, groupId, name)); - break; - case TYPE_CLOSED_CAPTIONS: - String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions); - String mimeType; - int accessibilityChannel; - if (instreamId.startsWith("CC")) { - mimeType = MimeTypes.APPLICATION_CEA608; - accessibilityChannel = Integer.parseInt(instreamId.substring(2)); - } else /* starts with SERVICE */ { - mimeType = MimeTypes.APPLICATION_CEA708; - accessibilityChannel = Integer.parseInt(instreamId.substring(7)); - } - if (muxedCaptionFormats == null) { - muxedCaptionFormats = new ArrayList<>(); - } - muxedCaptionFormats.add( - new Format.Builder() - .setId(formatId) - .setLabel(name) - .setLanguage(language) - .setSelectionFlags(selectionFlags) - .setRoleFlags(roleFlags) - .setAverageBitrate(Format.NO_VALUE) - .setPeakBitrate(Format.NO_VALUE) - .setCodecs(null) - .setContainerMimeType(null) - .setSampleMimeType(mimeType) - .setAccessibilityChannel(accessibilityChannel) - .build()); - - // TODO: Remove muxedCaptionFormats and add a Rendition with a null uri to closedCaptions. - break; - default: - // Do nothing. - break; - } - } - - if (noClosedCaptions) { - muxedCaptionFormats = Collections.emptyList(); - } - - return new CustomHlsMasterPlaylist( - URLEncoder.encode(baseUri, "utf-8"), - tags, - deduplicatedVariants, - videos, - audios, - subtitles, - closedCaptions, - muxedAudioFormat, - muxedCaptionFormats, - hasIndependentSegmentsTag, - variableDefinitions, - sessionKeyDrmInitData); - } - - private static CustomHlsMasterPlaylist.Variant getVariantWithAudioGroup(ArrayList variants, String groupId) { - for (int i = 0; i < variants.size(); i++) { - CustomHlsMasterPlaylist.Variant variant = variants.get(i); - if (groupId.equals(variant.audioGroupId)) { - return variant; - } - } - return null; - } - - private static CustomHlsMasterPlaylist.Variant getVariantWithVideoGroup(ArrayList variants, String groupId) { - for (int i = 0; i < variants.size(); i++) { - CustomHlsMasterPlaylist.Variant variant = variants.get(i); - if (groupId.equals(variant.videoGroupId)) { - return variant; - } - } - return null; - } - - private static HlsMediaPlaylist parseMediaPlaylist( - CustomHlsMasterPlaylist masterPlaylist, CustomHlsPlaylistParser.LineIterator iterator, String baseUri) throws IOException { - @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; - long startOffsetUs = C.TIME_UNSET; - long mediaSequence = 0; - int version = 1; // Default version == 1. - long targetDurationUs = C.TIME_UNSET; - boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments; - boolean hasEndTag = false; - HlsMediaPlaylist.Segment initializationSegment = null; - HashMap variableDefinitions = new HashMap<>(); - List segments = new ArrayList<>(); - List tags = new ArrayList<>(); - - long segmentDurationUs = 0; - String segmentTitle = ""; - boolean hasDiscontinuitySequence = false; - int playlistDiscontinuitySequence = 0; - int relativeDiscontinuitySequence = 0; - long playlistStartTimeUs = 0; - long segmentStartTimeUs = 0; - long segmentByteRangeOffset = 0; - long segmentByteRangeLength = C.LENGTH_UNSET; - long segmentMediaSequence = 0; - boolean hasGapTag = false; - - DrmInitData playlistProtectionSchemes = null; - String fullSegmentEncryptionKeyUri = null; - String fullSegmentEncryptionIV = null; - TreeMap currentSchemeDatas = new TreeMap<>(); - String encryptionScheme = null; - DrmInitData cachedDrmInitData = null; - List trailingParts = new ArrayList<>(); - Map renditionReports = new HashMap<>(); - HlsMediaPlaylist.ServerControl serverControl = new HlsMediaPlaylist.ServerControl(0,false,0,0,false); - List updatedParts = new ArrayList<>(); - - String line; - while (iterator.hasNext()) { - line = iterator.next(); - - Log.i("MusicService", "Player : Parser : Line 2: " + line); - - if (line.startsWith(TAG_PREFIX)) { - // We expose all tags through the playlist. - tags.add(line); - } - - if (line.startsWith(TAG_PLAYLIST_TYPE)) { - String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE, variableDefinitions); - if ("VOD".equals(playlistTypeString)) { - playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD; - } else if ("EVENT".equals(playlistTypeString)) { - playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT; - } - } else if (line.startsWith(TAG_START)) { - startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND); - } else if (line.startsWith(TAG_INIT_SEGMENT)) { - String uri = parseStringAttr(line, REGEX_URI, variableDefinitions); - String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions); - if (byteRange != null) { - String[] splitByteRange = byteRange.split("@"); - segmentByteRangeLength = Long.parseLong(splitByteRange[0]); - if (splitByteRange.length > 1) { - segmentByteRangeOffset = Long.parseLong(splitByteRange[1]); - } - } - if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) { - // See RFC 8216, Section 4.3.2.5. - throw ParserException.createForUnsupportedContainerFeature( - "The encryption IV attribute must be present when an initialization segment is " - + "encrypted with METHOD=AES-128."); - } - initializationSegment = - new HlsMediaPlaylist.Segment( - uri, - segmentByteRangeOffset, - segmentByteRangeLength, - fullSegmentEncryptionKeyUri, - fullSegmentEncryptionIV); - segmentByteRangeOffset = 0; - segmentByteRangeLength = C.LENGTH_UNSET; - } else if (line.startsWith(TAG_TARGET_DURATION)) { - targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND; - } else if (line.startsWith(TAG_MEDIA_SEQUENCE)) { - mediaSequence = parseLongAttr(line, REGEX_MEDIA_SEQUENCE); - segmentMediaSequence = mediaSequence; - } else if (line.startsWith(TAG_VERSION)) { - version = parseIntAttr(line, REGEX_VERSION); - } else if (line.startsWith(TAG_DEFINE)) { - String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions); - if (importName != null) { - String value = masterPlaylist.variableDefinitions.get(importName); - if (value != null) { - variableDefinitions.put(importName, value); - } else { - // The master playlist does not declare the imported variable. Ignore. - } - } else { - variableDefinitions.put( - parseStringAttr(line, REGEX_NAME, variableDefinitions), - parseStringAttr(line, REGEX_VALUE, variableDefinitions)); - } - } else if (line.startsWith(TAG_MEDIA_DURATION)) { - segmentDurationUs = - (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND); - segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions); - } else if (line.startsWith(TAG_KEY)) { - String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions); - String keyFormat = - parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions); - fullSegmentEncryptionKeyUri = null; - fullSegmentEncryptionIV = null; - if (METHOD_NONE.equals(method)) { - currentSchemeDatas.clear(); - cachedDrmInitData = null; - } else /* !METHOD_NONE.equals(method) */ { - fullSegmentEncryptionIV = parseOptionalStringAttr(line, REGEX_IV, variableDefinitions); - if (KEYFORMAT_IDENTITY.equals(keyFormat)) { - if (METHOD_AES_128.equals(method)) { - // The segment is fully encrypted using an identity key. - fullSegmentEncryptionKeyUri = parseStringAttr(line, REGEX_URI, variableDefinitions); - } else { - // Do nothing. Samples are encrypted using an identity key, but this is not supported. - // Hopefully, a traditional DRM alternative is also provided. - } - } else { - if (encryptionScheme == null) { - encryptionScheme = parseEncryptionScheme(method); - } - DrmInitData.SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions); - if (schemeData != null) { - cachedDrmInitData = null; - currentSchemeDatas.put(keyFormat, schemeData); - } - } - } - } else if (line.startsWith(TAG_BYTERANGE)) { - String byteRange = parseStringAttr(line, REGEX_BYTERANGE, variableDefinitions); - String[] splitByteRange = byteRange.split("@"); - segmentByteRangeLength = Long.parseLong(splitByteRange[0]); - if (splitByteRange.length > 1) { - segmentByteRangeOffset = Long.parseLong(splitByteRange[1]); - } - } else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) { - hasDiscontinuitySequence = true; - playlistDiscontinuitySequence = Integer.parseInt(line.substring(line.indexOf(':') + 1)); - } else if (line.equals(TAG_DISCONTINUITY)) { - relativeDiscontinuitySequence++; - } else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) { - if (playlistStartTimeUs == 0) { - long programDatetimeUs = - Util.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1))); - playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs; - } - } else if (line.equals(TAG_GAP)) { - hasGapTag = true; - } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) { - hasIndependentSegmentsTag = true; - } else if (line.equals(TAG_ENDLIST)) { - hasEndTag = true; - } else if (!line.startsWith("#")) { - String segmentEncryptionIV; - if (fullSegmentEncryptionKeyUri == null) { - segmentEncryptionIV = null; - } else if (fullSegmentEncryptionIV != null) { - segmentEncryptionIV = fullSegmentEncryptionIV; - } else { - segmentEncryptionIV = Long.toHexString(segmentMediaSequence); - } - - segmentMediaSequence++; - if (segmentByteRangeLength == C.LENGTH_UNSET) { - segmentByteRangeOffset = 0; - } - - if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) { - DrmInitData.SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new DrmInitData.SchemeData[0]); - cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas); - if (playlistProtectionSchemes == null) { - DrmInitData.SchemeData[] playlistSchemeDatas = new DrmInitData.SchemeData[schemeDatas.length]; - for (int i = 0; i < schemeDatas.length; i++) { - playlistSchemeDatas[i] = schemeDatas[i].copyWithData(null); - } - playlistProtectionSchemes = new DrmInitData(encryptionScheme, playlistSchemeDatas); - } - } - - line = URLEncoder.encode(line, "utf-8"); - - Log.i("MediaService", "Player : Parser : Line 3: " + replaceVariableReferences(line, variableDefinitions)); - - segments.add( - new HlsMediaPlaylist.Segment( - replaceVariableReferences(line, variableDefinitions), - initializationSegment, - segmentTitle, - segmentDurationUs, - relativeDiscontinuitySequence, - segmentStartTimeUs, - cachedDrmInitData, - fullSegmentEncryptionKeyUri, - segmentEncryptionIV, - segmentByteRangeOffset, - segmentByteRangeLength, - hasGapTag, - updatedParts)); - segmentStartTimeUs += segmentDurationUs; - segmentDurationUs = 0; - segmentTitle = ""; - if (segmentByteRangeLength != C.LENGTH_UNSET) { - segmentByteRangeOffset += segmentByteRangeLength; - } - segmentByteRangeLength = C.LENGTH_UNSET; - hasGapTag = false; - } - } - - return new HlsMediaPlaylist( - playlistType, - baseUri, - tags, - startOffsetUs, - false, - playlistStartTimeUs, - hasDiscontinuitySequence, - playlistDiscontinuitySequence, - mediaSequence, - version, - targetDurationUs, - targetDurationUs, - hasIndependentSegmentsTag, - hasEndTag, - /* hasProgramDateTime= */ playlistStartTimeUs != 0, - playlistProtectionSchemes, - segments, trailingParts, serverControl, renditionReports); - } - - @C.SelectionFlags - private static int parseSelectionFlags(String line) { - int flags = 0; - if (parseOptionalBooleanAttribute(line, REGEX_DEFAULT, false)) { - flags |= C.SELECTION_FLAG_DEFAULT; - } - if (parseOptionalBooleanAttribute(line, REGEX_FORCED, false)) { - flags |= C.SELECTION_FLAG_FORCED; - } - if (parseOptionalBooleanAttribute(line, REGEX_AUTOSELECT, false)) { - flags |= C.SELECTION_FLAG_AUTOSELECT; - } - return flags; - } - - @C.RoleFlags - private static int parseRoleFlags(String line, Map variableDefinitions) { - String concatenatedCharacteristics = - parseOptionalStringAttr(line, REGEX_CHARACTERISTICS, variableDefinitions); - if (TextUtils.isEmpty(concatenatedCharacteristics)) { - return 0; - } - String[] characteristics = Util.split(concatenatedCharacteristics, ","); - @C.RoleFlags int roleFlags = 0; - if (Util.contains(characteristics, "public.accessibility.describes-video")) { - roleFlags |= C.ROLE_FLAG_DESCRIBES_VIDEO; - } - if (Util.contains(characteristics, "public.accessibility.transcribes-spoken-dialog")) { - roleFlags |= C.ROLE_FLAG_TRANSCRIBES_DIALOG; - } - if (Util.contains(characteristics, "public.accessibility.describes-music-and-sound")) { - roleFlags |= C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; - } - if (Util.contains(characteristics, "public.easy-to-read")) { - roleFlags |= C.ROLE_FLAG_EASY_TO_READ; - } - return roleFlags; - } - - private static int parseChannelsAttribute(String line, Map variableDefinitions) { - String channelsString = parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions); - return channelsString != null - ? Integer.parseInt(Util.splitAtFirst(channelsString, "/")[0]) - : Format.NO_VALUE; - } - - @Nullable - private static DrmInitData.SchemeData parseDrmSchemeData( - String line, String keyFormat, Map variableDefinitions) - throws ParserException { - String keyFormatVersions = - parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions); - if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) { - String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions); - return new DrmInitData.SchemeData( - C.WIDEVINE_UUID, - MimeTypes.VIDEO_MP4, - Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT)); - } else if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) { - return new DrmInitData.SchemeData(C.WIDEVINE_UUID, "hls", Util.getUtf8Bytes(line)); - } else if (KEYFORMAT_PLAYREADY.equals(keyFormat) && "1".equals(keyFormatVersions)) { - String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions); - byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT); - byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data); - return new DrmInitData.SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData); - } - 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 { - String value = parseOptionalStringAttr(line, pattern, variableDefinitions); - if (value != null) { - return value; - } else { - throw ParserException.createForUnsupportedContainerFeature("Couldn't match " + pattern.pattern() + " in " + line); - } - } - - private static @Nullable - String parseOptionalStringAttr( - String line, Pattern pattern, Map variableDefinitions) { - return parseOptionalStringAttr(line, pattern, null, variableDefinitions); - } - - private static String parseOptionalStringAttr( - String line, - Pattern pattern, - String defaultValue, - Map variableDefinitions) { - Matcher matcher = pattern.matcher(line); - String value = matcher.find() ? matcher.group(1) : defaultValue; - return variableDefinitions.isEmpty() || value == null - ? value - : replaceVariableReferences(value, variableDefinitions); - } - - private static String replaceVariableReferences( - String string, Map variableDefinitions) { - Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string); - // TODO: Replace StringBuffer with StringBuilder once Java 9 is available. - StringBuffer stringWithReplacements = new StringBuffer(); - while (matcher.find()) { - String groupName = matcher.group(1); - if (variableDefinitions.containsKey(groupName)) { - matcher.appendReplacement( - stringWithReplacements, Matcher.quoteReplacement(variableDefinitions.get(groupName))); - } else { - // The variable is not defined. The value is ignored. - } - } - matcher.appendTail(stringWithReplacements); - return stringWithReplacements.toString(); - } - - private static boolean parseOptionalBooleanAttribute( - String line, Pattern pattern, boolean defaultValue) { - Matcher matcher = pattern.matcher(line); - if (matcher.find()) { - return matcher.group(1).equals(BOOLEAN_TRUE); - } - return defaultValue; - } - - private static Pattern compileBooleanAttrPattern(String attribute) { - return Pattern.compile(attribute + "=(" + BOOLEAN_FALSE + "|" + BOOLEAN_TRUE + ")"); - } - - private static class LineIterator { - - private final BufferedReader reader; - private final Queue extraLines; - - private String next; - - public LineIterator(Queue extraLines, BufferedReader reader) { - this.extraLines = extraLines; - this.reader = reader; - } - - public boolean hasNext() throws IOException { - if (next != null) { - return true; - } - if (!extraLines.isEmpty()) { - next = extraLines.poll(); - return true; - } - while ((next = reader.readLine()) != null) { - next = next.trim(); - if (!next.isEmpty()) { - return true; - } - } - return false; - } - - public String next() throws IOException { - String result = null; - if (hasNext()) { - result = next; - next = null; - } - return result; - } - - } - -} \ No newline at end of file +//package br.com.suamusica.player.media.parser; +// +//import java.io.BufferedReader; +//import java.io.IOException; +//import java.io.InputStream; +//import java.io.InputStreamReader; +//import java.net.URLEncoder; +//import java.util.ArrayDeque; +//import java.util.ArrayList; +//import java.util.Collections; +//import java.util.HashMap; +//import java.util.HashSet; +//import java.util.List; +//import java.util.Map; +//import java.util.Queue; +//import java.util.TreeMap; +//import java.util.regex.Matcher; +//import java.util.regex.Pattern; +// +//import androidx.media3.common.C; +//import androidx.media3.common.Format; +//import androidx.media3.common.ParserException; +//import androidx.media3.common.DrmInitData; +//import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; +//import androidx.media3.common.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 androidx.media3.common.MimeTypes; +//import com.google.android.exoplayer2.util.UriUtil; +//import com.google.android.exoplayer2.util.Util; +// +//import android.util.Log; +// +//import android.net.Uri; +//import android.text.TextUtils; +//import android.util.Base64; +// +//import androidx.annotation.Nullable; +// +//public class CustomHlsPlaylistParser implements ParsingLoadable.Parser { +// +// private static final String PLAYLIST_HEADER = "#EXTM3U"; +// +// private static final String TAG_PREFIX = "#EXT"; +// +// private static final String TAG_VERSION = "#EXT-X-VERSION"; +// private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE"; +// private static final String TAG_DEFINE = "#EXT-X-DEFINE"; +// private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF"; +// private static final String TAG_MEDIA = "#EXT-X-MEDIA"; +// private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION"; +// private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY"; +// private static final String TAG_DISCONTINUITY_SEQUENCE = "#EXT-X-DISCONTINUITY-SEQUENCE"; +// private static final String TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME"; +// private static final String TAG_INIT_SEGMENT = "#EXT-X-MAP"; +// private static final String TAG_INDEPENDENT_SEGMENTS = "#EXT-X-INDEPENDENT-SEGMENTS"; +// private static final String TAG_MEDIA_DURATION = "#EXTINF"; +// private static final String TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE"; +// private static final String TAG_START = "#EXT-X-START"; +// private static final String TAG_ENDLIST = "#EXT-X-ENDLIST"; +// private static final String TAG_KEY = "#EXT-X-KEY"; +// private static final String TAG_SESSION_KEY = "#EXT-X-SESSION-KEY"; +// private static final String TAG_BYTERANGE = "#EXT-X-BYTERANGE"; +// private static final String TAG_GAP = "#EXT-X-GAP"; +// +// private static final String TYPE_AUDIO = "AUDIO"; +// private static final String TYPE_VIDEO = "VIDEO"; +// private static final String TYPE_SUBTITLES = "SUBTITLES"; +// private static final String TYPE_CLOSED_CAPTIONS = "CLOSED-CAPTIONS"; +// +// private static final String METHOD_NONE = "NONE"; +// private static final String METHOD_AES_128 = "AES-128"; +// private static final String METHOD_SAMPLE_AES = "SAMPLE-AES"; +// // Replaced by METHOD_SAMPLE_AES_CTR. Keep for backward compatibility. +// private static final String METHOD_SAMPLE_AES_CENC = "SAMPLE-AES-CENC"; +// private static final String METHOD_SAMPLE_AES_CTR = "SAMPLE-AES-CTR"; +// private static final String KEYFORMAT_PLAYREADY = "com.microsoft.playready"; +// private static final String KEYFORMAT_IDENTITY = "identity"; +// private static final String KEYFORMAT_WIDEVINE_PSSH_BINARY = +// "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"; +// private static final String KEYFORMAT_WIDEVINE_PSSH_JSON = "com.widevine"; +// +// private static final String BOOLEAN_TRUE = "YES"; +// private static final String BOOLEAN_FALSE = "NO"; +// +// private static final String ATTR_CLOSED_CAPTIONS_NONE = "CLOSED-CAPTIONS=NONE"; +// +// private static final Pattern REGEX_AVERAGE_BANDWIDTH = +// Pattern.compile("AVERAGE-BANDWIDTH=(\\d+)\\b"); +// private static final Pattern REGEX_VIDEO = Pattern.compile("VIDEO=\"(.+?)\""); +// private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\""); +// private static final Pattern REGEX_SUBTITLES = Pattern.compile("SUBTITLES=\"(.+?)\""); +// private static final Pattern REGEX_CLOSED_CAPTIONS = Pattern.compile("CLOSED-CAPTIONS=\"(.+?)\""); +// private static final Pattern REGEX_BANDWIDTH = Pattern.compile("[^-]BANDWIDTH=(\\d+)\\b"); +// private static final Pattern REGEX_CHANNELS = Pattern.compile("CHANNELS=\"(.+?)\""); +// private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\""); +// private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)"); +// private static final Pattern REGEX_FRAME_RATE = Pattern.compile("FRAME-RATE=([\\d\\.]+)\\b"); +// private static final Pattern REGEX_TARGET_DURATION = Pattern.compile(TAG_TARGET_DURATION +// + ":(\\d+)\\b"); +// private static final Pattern REGEX_VERSION = Pattern.compile(TAG_VERSION + ":(\\d+)\\b"); +// private static final Pattern REGEX_PLAYLIST_TYPE = Pattern.compile(TAG_PLAYLIST_TYPE +// + ":(.+)\\b"); +// private static final Pattern REGEX_MEDIA_SEQUENCE = Pattern.compile(TAG_MEDIA_SEQUENCE +// + ":(\\d+)\\b"); +// private static final Pattern REGEX_MEDIA_DURATION = Pattern.compile(TAG_MEDIA_DURATION +// + ":([\\d\\.]+)\\b"); +// private static final Pattern REGEX_MEDIA_TITLE = +// Pattern.compile(TAG_MEDIA_DURATION + ":[\\d\\.]+\\b,(.+)"); +// private static final Pattern REGEX_TIME_OFFSET = Pattern.compile("TIME-OFFSET=(-?[\\d\\.]+)\\b"); +// private static final Pattern REGEX_BYTERANGE = Pattern.compile(TAG_BYTERANGE +// + ":(\\d+(?:@\\d+)?)\\b"); +// private static final Pattern REGEX_ATTR_BYTERANGE = +// Pattern.compile("BYTERANGE=\"(\\d+(?:@\\d+)?)\\b\""); +// private static final Pattern REGEX_METHOD = +// Pattern.compile( +// "METHOD=(" +// + METHOD_NONE +// + "|" +// + METHOD_AES_128 +// + "|" +// + METHOD_SAMPLE_AES +// + "|" +// + METHOD_SAMPLE_AES_CENC +// + "|" +// + METHOD_SAMPLE_AES_CTR +// + ")" +// + "\\s*(?:,|$)"); +// private static final Pattern REGEX_KEYFORMAT = Pattern.compile("KEYFORMAT=\"(.+?)\""); +// private static final Pattern REGEX_KEYFORMATVERSIONS = +// Pattern.compile("KEYFORMATVERSIONS=\"(.+?)\""); +// private static final Pattern REGEX_URI = Pattern.compile("URI=\"(.+?)\""); +// private static final Pattern REGEX_IV = Pattern.compile("IV=([^,.*]+)"); +// private static final Pattern REGEX_TYPE = Pattern.compile("TYPE=(" + TYPE_AUDIO + "|" + TYPE_VIDEO +// + "|" + TYPE_SUBTITLES + "|" + TYPE_CLOSED_CAPTIONS + ")"); +// private static final Pattern REGEX_LANGUAGE = Pattern.compile("LANGUAGE=\"(.+?)\""); +// private static final Pattern REGEX_NAME = Pattern.compile("NAME=\"(.+?)\""); +// private static final Pattern REGEX_GROUP_ID = Pattern.compile("GROUP-ID=\"(.+?)\""); +// private static final Pattern REGEX_CHARACTERISTICS = Pattern.compile("CHARACTERISTICS=\"(.+?)\""); +// private static final Pattern REGEX_INSTREAM_ID = +// Pattern.compile("INSTREAM-ID=\"((?:CC|SERVICE)\\d+)\""); +// private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT"); +// private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT"); +// private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED"); +// private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\""); +// private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\""); +// private static final Pattern REGEX_VARIABLE_REFERENCE = +// Pattern.compile("\\{\\$([a-zA-Z0-9\\-_]+)\\}"); +// +// private final CustomHlsMasterPlaylist masterPlaylist; +// +// /** +// * Creates an instance where media playlists are parsed without inheriting attributes from a +// * master playlist. +// */ +// public CustomHlsPlaylistParser() { +// this(CustomHlsMasterPlaylist.EMPTY); +// } +// +// /** +// * Creates an instance where parsed media playlists inherit attributes from the given master +// * playlist. +// * +// * @param masterPlaylist The master playlist from which media playlists will inherit attributes. +// */ +// public CustomHlsPlaylistParser(CustomHlsMasterPlaylist masterPlaylist) { +// this.masterPlaylist = masterPlaylist; +// } +// +// @Override +// public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { +// Log.i("MusicService", "Player : Parser..."); +// BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); +// Queue extraLines = new ArrayDeque<>(); +// String line; +// try { +// if (!checkPlaylistHeader(reader)) { +// throw new UnrecognizedInputFormatException("Input does not start with the #EXTM3U header.", +// uri); +// } +// while ((line = reader.readLine()) != null) { +// line = line.trim(); +// Log.i("MusicService", "Player : Parser : Line 0: " + line); +// if (line.isEmpty()) { +// // Do nothing. +// } else if (line.startsWith(TAG_STREAM_INF)) { +// extraLines.add(line); +// return parseMasterPlaylist(new CustomHlsPlaylistParser.LineIterator(extraLines, reader), uri.toString()); +// } else if (line.startsWith(TAG_TARGET_DURATION) +// || line.startsWith(TAG_MEDIA_SEQUENCE) +// || line.startsWith(TAG_MEDIA_DURATION) +// || line.startsWith(TAG_KEY) +// || line.startsWith(TAG_BYTERANGE) +// || line.equals(TAG_DISCONTINUITY) +// || line.equals(TAG_DISCONTINUITY_SEQUENCE) +// || line.equals(TAG_ENDLIST)) { +// extraLines.add(line); +// return parseMediaPlaylist( +// masterPlaylist, new CustomHlsPlaylistParser.LineIterator(extraLines, reader), uri.toString()); +// } else { +// extraLines.add(line); +// } +// } +// } finally { +// Util.closeQuietly(reader); +// } +// 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) { +// if (reader.read() != 0xBB || reader.read() != 0xBF) { +// return false; +// } +// // The playlist contains a Byte Order Mark, which gets discarded. +// last = reader.read(); +// } +// last = skipIgnorableWhitespace(reader, true, last); +// int playlistHeaderLength = PLAYLIST_HEADER.length(); +// for (int i = 0; i < playlistHeaderLength; i++) { +// if (last != PLAYLIST_HEADER.charAt(i)) { +// return false; +// } +// last = reader.read(); +// } +// last = skipIgnorableWhitespace(reader, false, last); +// 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))) { +// c = reader.read(); +// } +// return c; +// } +// +// private static CustomHlsMasterPlaylist parseMasterPlaylist(CustomHlsPlaylistParser.LineIterator iterator, String baseUri) +// throws IOException { +// HashMap> urlToVariantInfos = new HashMap<>(); +// HashMap variableDefinitions = new HashMap<>(); +// ArrayList variants = new ArrayList<>(); +// ArrayList videos = new ArrayList<>(); +// ArrayList audios = new ArrayList<>(); +// ArrayList subtitles = new ArrayList<>(); +// ArrayList closedCaptions = new ArrayList<>(); +// ArrayList mediaTags = new ArrayList<>(); +// ArrayList sessionKeyDrmInitData = new ArrayList<>(); +// ArrayList tags = new ArrayList<>(); +// Format muxedAudioFormat = null; +// List muxedCaptionFormats = null; +// boolean noClosedCaptions = false; +// boolean hasIndependentSegmentsTag = false; +// +// String line; +// while (iterator.hasNext()) { +// line = iterator.next(); +// +// Log.i("MusicService", "Player : Parser : Line 1 : " + line); +// +// if (line.startsWith(TAG_PREFIX)) { +// // We expose all tags through the playlist. +// tags.add(line); +// } +// +// if (line.startsWith(TAG_DEFINE)) { +// variableDefinitions.put( +// /* key= */ parseStringAttr(line, REGEX_NAME, variableDefinitions), +// /* value= */ parseStringAttr(line, REGEX_VALUE, variableDefinitions)); +// } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) { +// hasIndependentSegmentsTag = true; +// } else if (line.startsWith(TAG_MEDIA)) { +// // Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF +// // tags. +// mediaTags.add(line); +// } else if (line.startsWith(TAG_SESSION_KEY)) { +// String keyFormat = +// parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions); +// DrmInitData.SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions); +// if (schemeData != null) { +// String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions); +// String scheme = parseEncryptionScheme(method); +// sessionKeyDrmInitData.add(new DrmInitData(scheme, schemeData)); +// } +// } else if (line.startsWith(TAG_STREAM_INF)) { +// noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE); +// int bitrate = parseIntAttr(line, REGEX_BANDWIDTH); +// String averageBandwidthString = +// parseOptionalStringAttr(line, REGEX_AVERAGE_BANDWIDTH, variableDefinitions); +// if (averageBandwidthString != null) { +// // If available, the average bandwidth attribute is used as the variant's bitrate. +// bitrate = Integer.parseInt(averageBandwidthString); +// } +// String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions); +// String resolutionString = +// parseOptionalStringAttr(line, REGEX_RESOLUTION, variableDefinitions); +// int width; +// int height; +// if (resolutionString != null) { +// String[] widthAndHeight = resolutionString.split("x"); +// width = Integer.parseInt(widthAndHeight[0]); +// height = Integer.parseInt(widthAndHeight[1]); +// if (width <= 0 || height <= 0) { +// // Resolution string is invalid. +// width = Format.NO_VALUE; +// height = Format.NO_VALUE; +// } +// } else { +// width = Format.NO_VALUE; +// height = Format.NO_VALUE; +// } +// float frameRate = Format.NO_VALUE; +// String frameRateString = +// parseOptionalStringAttr(line, REGEX_FRAME_RATE, variableDefinitions); +// if (frameRateString != null) { +// frameRate = Float.parseFloat(frameRateString); +// } +// String videoGroupId = parseOptionalStringAttr(line, REGEX_VIDEO, variableDefinitions); +// String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO, variableDefinitions); +// String subtitlesGroupId = +// parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions); +// String closedCaptionsGroupId = +// parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions); +// line = +// replaceVariableReferences( +// iterator.next(), variableDefinitions); // #EXT-X-STREAM-INF's URI. +// Uri uri = UriUtil.resolveToUri(baseUri, line); +// Format format = +// new Format.Builder() +// .setId(Integer.toString(variants.size())) +// .setLabel(null) +// .setSelectionFlags(0) +// .setRoleFlags(0) +// .setAverageBitrate(bitrate) +// .setPeakBitrate(bitrate) +// .setCodecs(codecs) +// .setMetadata(null) +// .setContainerMimeType(MimeTypes.APPLICATION_M3U8) +// .setSampleMimeType(null) +// .setInitializationData(null) +// .setWidth(width) +// .setHeight(height) +// .setFrameRate(frameRate) +// .build(); +// CustomHlsMasterPlaylist.Variant variant = +// new CustomHlsMasterPlaylist.Variant( +// uri, format, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId); +// variants.add(variant); +// ArrayList variantInfosForUrl = urlToVariantInfos.get(uri); +// if (variantInfosForUrl == null) { +// variantInfosForUrl = new ArrayList<>(); +// urlToVariantInfos.put(uri, variantInfosForUrl); +// } +// variantInfosForUrl.add(new HlsTrackMetadataEntry.VariantInfo(bitrate, bitrate, videoGroupId, audioGroupId, subtitlesGroupId, closedCaptionsGroupId)); +// } +// } +// +// // TODO: Don't deduplicate variants by URL. +// ArrayList deduplicatedVariants = new ArrayList<>(); +// HashSet urlsInDeduplicatedVariants = new HashSet<>(); +// for (int i = 0; i < variants.size(); i++) { +// CustomHlsMasterPlaylist.Variant variant = variants.get(i); +// if (urlsInDeduplicatedVariants.add(variant.url)) { +// Assertions.checkState(variant.format.metadata == null); +// HlsTrackMetadataEntry hlsMetadataEntry = +// new HlsTrackMetadataEntry( +// /* groupId= */ null, /* name= */ null, urlToVariantInfos.get(variant.url)); +// deduplicatedVariants.add( +// variant.copyWithFormat( +// variant.format.buildUpon().setMetadata((new Metadata(hlsMetadataEntry))).build())); +// } +// } +// +// for (int i = 0; i < mediaTags.size(); i++) { +// line = mediaTags.get(i); +// String groupId = parseStringAttr(line, REGEX_GROUP_ID, variableDefinitions); +// String name = parseStringAttr(line, REGEX_NAME, variableDefinitions); +// String referenceUri = parseOptionalStringAttr(line, REGEX_URI, variableDefinitions); +// Uri uri = referenceUri == null ? null : UriUtil.resolveToUri(baseUri, referenceUri); +// String language = parseOptionalStringAttr(line, REGEX_LANGUAGE, variableDefinitions); +// @C.SelectionFlags int selectionFlags = parseSelectionFlags(line); +// @C.RoleFlags int roleFlags = parseRoleFlags(line, variableDefinitions); +// String formatId = groupId + ":" + name; +// Format format; +// Metadata metadata = +// new Metadata(new HlsTrackMetadataEntry(groupId, name, new ArrayList())); +// switch (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) { +// case TYPE_VIDEO: +// CustomHlsMasterPlaylist.Variant variant = getVariantWithVideoGroup(variants, groupId); +// String codecs = null; +// int width = Format.NO_VALUE; +// int height = Format.NO_VALUE; +// float frameRate = Format.NO_VALUE; +// if (variant != null) { +// Format variantFormat = variant.format; +// codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); +// width = variantFormat.width; +// height = variantFormat.height; +// frameRate = variantFormat.frameRate; +// } +// String sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null; +// format = +// new Format.Builder() +// .setId(formatId) +// .setLabel(name) +// .setSelectionFlags(selectionFlags) +// .setRoleFlags(roleFlags) +// .setAverageBitrate(Format.NO_VALUE) +// .setPeakBitrate(Format.NO_VALUE) +// .setCodecs(codecs) +// .setMetadata(metadata) +// .setContainerMimeType(MimeTypes.APPLICATION_M3U8) +// .setSampleMimeType(sampleMimeType) +// .setInitializationData(null) +// .setWidth(width) +// .setHeight(height) +// .setFrameRate(frameRate) +// .build(); +// //.copyWithMetadata(metadata); +// if (uri == null) { +// // TODO: Remove this case and add a Rendition with a null uri to videos. +// } else { +// videos.add(new CustomHlsMasterPlaylist.Rendition(uri, format, groupId, name)); +// } +// break; +// case TYPE_AUDIO: +// variant = getVariantWithAudioGroup(variants, groupId); +// codecs = +// variant != null +// ? Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_AUDIO) +// : null; +// sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null; +// int channelCount = parseChannelsAttribute(line, variableDefinitions); +// format = new Format.Builder() +// .setId(formatId) +// .setLabel(name) +// .setLanguage(language) +// .setSelectionFlags(selectionFlags) +// .setRoleFlags(roleFlags) +// .setAverageBitrate(Format.NO_VALUE) +// .setPeakBitrate(Format.NO_VALUE) +// .setCodecs(codecs) +// .setMetadata(metadata) +// .setContainerMimeType(MimeTypes.APPLICATION_M3U8) +// .setSampleMimeType(sampleMimeType) +// .setInitializationData(null) +// .setChannelCount(channelCount) +// .setSampleRate(Format.NO_VALUE) +// .build(); +// if (uri == null) { +// // TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios. +// muxedAudioFormat = format; +// } else { +// audios.add(new CustomHlsMasterPlaylist.Rendition(uri, format.buildUpon().setMetadata(metadata).build(), groupId, name)); +// } +// break; +// case TYPE_SUBTITLES: +// format = +// new Format.Builder() +// .setId(formatId) +// .setLabel(name) +// .setLanguage(language) +// .setSelectionFlags(selectionFlags) +// .setRoleFlags(roleFlags) +// .setAverageBitrate(Format.NO_VALUE) +// .setPeakBitrate(Format.NO_VALUE) +// .setCodecs(null) +// .setContainerMimeType(MimeTypes.APPLICATION_M3U8) +// .setSampleMimeType(MimeTypes.TEXT_VTT) +// .setMetadata(metadata) +// .build(); +// subtitles.add(new CustomHlsMasterPlaylist.Rendition(uri, format, groupId, name)); +// break; +// case TYPE_CLOSED_CAPTIONS: +// String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions); +// String mimeType; +// int accessibilityChannel; +// if (instreamId.startsWith("CC")) { +// mimeType = MimeTypes.APPLICATION_CEA608; +// accessibilityChannel = Integer.parseInt(instreamId.substring(2)); +// } else /* starts with SERVICE */ { +// mimeType = MimeTypes.APPLICATION_CEA708; +// accessibilityChannel = Integer.parseInt(instreamId.substring(7)); +// } +// if (muxedCaptionFormats == null) { +// muxedCaptionFormats = new ArrayList<>(); +// } +// muxedCaptionFormats.add( +// new Format.Builder() +// .setId(formatId) +// .setLabel(name) +// .setLanguage(language) +// .setSelectionFlags(selectionFlags) +// .setRoleFlags(roleFlags) +// .setAverageBitrate(Format.NO_VALUE) +// .setPeakBitrate(Format.NO_VALUE) +// .setCodecs(null) +// .setContainerMimeType(null) +// .setSampleMimeType(mimeType) +// .setAccessibilityChannel(accessibilityChannel) +// .build()); +// +// // TODO: Remove muxedCaptionFormats and add a Rendition with a null uri to closedCaptions. +// break; +// default: +// // Do nothing. +// break; +// } +// } +// +// if (noClosedCaptions) { +// muxedCaptionFormats = Collections.emptyList(); +// } +// +// return new CustomHlsMasterPlaylist( +// URLEncoder.encode(baseUri, "utf-8"), +// tags, +// deduplicatedVariants, +// videos, +// audios, +// subtitles, +// closedCaptions, +// muxedAudioFormat, +// muxedCaptionFormats, +// hasIndependentSegmentsTag, +// variableDefinitions, +// sessionKeyDrmInitData); +// } +// +// private static CustomHlsMasterPlaylist.Variant getVariantWithAudioGroup(ArrayList variants, String groupId) { +// for (int i = 0; i < variants.size(); i++) { +// CustomHlsMasterPlaylist.Variant variant = variants.get(i); +// if (groupId.equals(variant.audioGroupId)) { +// return variant; +// } +// } +// return null; +// } +// +// private static CustomHlsMasterPlaylist.Variant getVariantWithVideoGroup(ArrayList variants, String groupId) { +// for (int i = 0; i < variants.size(); i++) { +// CustomHlsMasterPlaylist.Variant variant = variants.get(i); +// if (groupId.equals(variant.videoGroupId)) { +// return variant; +// } +// } +// return null; +// } +// +// private static HlsMediaPlaylist parseMediaPlaylist( +// CustomHlsMasterPlaylist masterPlaylist, CustomHlsPlaylistParser.LineIterator iterator, String baseUri) throws IOException { +// @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; +// long startOffsetUs = C.TIME_UNSET; +// long mediaSequence = 0; +// int version = 1; // Default version == 1. +// long targetDurationUs = C.TIME_UNSET; +// boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments; +// boolean hasEndTag = false; +// HlsMediaPlaylist.Segment initializationSegment = null; +// HashMap variableDefinitions = new HashMap<>(); +// List segments = new ArrayList<>(); +// List tags = new ArrayList<>(); +// +// long segmentDurationUs = 0; +// String segmentTitle = ""; +// boolean hasDiscontinuitySequence = false; +// int playlistDiscontinuitySequence = 0; +// int relativeDiscontinuitySequence = 0; +// long playlistStartTimeUs = 0; +// long segmentStartTimeUs = 0; +// long segmentByteRangeOffset = 0; +// long segmentByteRangeLength = C.LENGTH_UNSET; +// long segmentMediaSequence = 0; +// boolean hasGapTag = false; +// +// DrmInitData playlistProtectionSchemes = null; +// String fullSegmentEncryptionKeyUri = null; +// String fullSegmentEncryptionIV = null; +// TreeMap currentSchemeDatas = new TreeMap<>(); +// String encryptionScheme = null; +// DrmInitData cachedDrmInitData = null; +// List trailingParts = new ArrayList<>(); +// Map renditionReports = new HashMap<>(); +// HlsMediaPlaylist.ServerControl serverControl = new HlsMediaPlaylist.ServerControl(0,false,0,0,false); +// List updatedParts = new ArrayList<>(); +// +// String line; +// while (iterator.hasNext()) { +// line = iterator.next(); +// +// Log.i("MusicService", "Player : Parser : Line 2: " + line); +// +// if (line.startsWith(TAG_PREFIX)) { +// // We expose all tags through the playlist. +// tags.add(line); +// } +// +// if (line.startsWith(TAG_PLAYLIST_TYPE)) { +// String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE, variableDefinitions); +// if ("VOD".equals(playlistTypeString)) { +// playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD; +// } else if ("EVENT".equals(playlistTypeString)) { +// playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT; +// } +// } else if (line.startsWith(TAG_START)) { +// startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND); +// } else if (line.startsWith(TAG_INIT_SEGMENT)) { +// String uri = parseStringAttr(line, REGEX_URI, variableDefinitions); +// String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions); +// if (byteRange != null) { +// String[] splitByteRange = byteRange.split("@"); +// segmentByteRangeLength = Long.parseLong(splitByteRange[0]); +// if (splitByteRange.length > 1) { +// segmentByteRangeOffset = Long.parseLong(splitByteRange[1]); +// } +// } +// if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) { +// // See RFC 8216, Section 4.3.2.5. +// throw ParserException.createForUnsupportedContainerFeature( +// "The encryption IV attribute must be present when an initialization segment is " +// + "encrypted with METHOD=AES-128."); +// } +// initializationSegment = +// new HlsMediaPlaylist.Segment( +// uri, +// segmentByteRangeOffset, +// segmentByteRangeLength, +// fullSegmentEncryptionKeyUri, +// fullSegmentEncryptionIV); +// segmentByteRangeOffset = 0; +// segmentByteRangeLength = C.LENGTH_UNSET; +// } else if (line.startsWith(TAG_TARGET_DURATION)) { +// targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND; +// } else if (line.startsWith(TAG_MEDIA_SEQUENCE)) { +// mediaSequence = parseLongAttr(line, REGEX_MEDIA_SEQUENCE); +// segmentMediaSequence = mediaSequence; +// } else if (line.startsWith(TAG_VERSION)) { +// version = parseIntAttr(line, REGEX_VERSION); +// } else if (line.startsWith(TAG_DEFINE)) { +// String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions); +// if (importName != null) { +// String value = masterPlaylist.variableDefinitions.get(importName); +// if (value != null) { +// variableDefinitions.put(importName, value); +// } else { +// // The master playlist does not declare the imported variable. Ignore. +// } +// } else { +// variableDefinitions.put( +// parseStringAttr(line, REGEX_NAME, variableDefinitions), +// parseStringAttr(line, REGEX_VALUE, variableDefinitions)); +// } +// } else if (line.startsWith(TAG_MEDIA_DURATION)) { +// segmentDurationUs = +// (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND); +// segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions); +// } else if (line.startsWith(TAG_KEY)) { +// String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions); +// String keyFormat = +// parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions); +// fullSegmentEncryptionKeyUri = null; +// fullSegmentEncryptionIV = null; +// if (METHOD_NONE.equals(method)) { +// currentSchemeDatas.clear(); +// cachedDrmInitData = null; +// } else /* !METHOD_NONE.equals(method) */ { +// fullSegmentEncryptionIV = parseOptionalStringAttr(line, REGEX_IV, variableDefinitions); +// if (KEYFORMAT_IDENTITY.equals(keyFormat)) { +// if (METHOD_AES_128.equals(method)) { +// // The segment is fully encrypted using an identity key. +// fullSegmentEncryptionKeyUri = parseStringAttr(line, REGEX_URI, variableDefinitions); +// } else { +// // Do nothing. Samples are encrypted using an identity key, but this is not supported. +// // Hopefully, a traditional DRM alternative is also provided. +// } +// } else { +// if (encryptionScheme == null) { +// encryptionScheme = parseEncryptionScheme(method); +// } +// DrmInitData.SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions); +// if (schemeData != null) { +// cachedDrmInitData = null; +// currentSchemeDatas.put(keyFormat, schemeData); +// } +// } +// } +// } else if (line.startsWith(TAG_BYTERANGE)) { +// String byteRange = parseStringAttr(line, REGEX_BYTERANGE, variableDefinitions); +// String[] splitByteRange = byteRange.split("@"); +// segmentByteRangeLength = Long.parseLong(splitByteRange[0]); +// if (splitByteRange.length > 1) { +// segmentByteRangeOffset = Long.parseLong(splitByteRange[1]); +// } +// } else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) { +// hasDiscontinuitySequence = true; +// playlistDiscontinuitySequence = Integer.parseInt(line.substring(line.indexOf(':') + 1)); +// } else if (line.equals(TAG_DISCONTINUITY)) { +// relativeDiscontinuitySequence++; +// } else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) { +// if (playlistStartTimeUs == 0) { +// long programDatetimeUs = +// Util.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1))); +// playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs; +// } +// } else if (line.equals(TAG_GAP)) { +// hasGapTag = true; +// } else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) { +// hasIndependentSegmentsTag = true; +// } else if (line.equals(TAG_ENDLIST)) { +// hasEndTag = true; +// } else if (!line.startsWith("#")) { +// String segmentEncryptionIV; +// if (fullSegmentEncryptionKeyUri == null) { +// segmentEncryptionIV = null; +// } else if (fullSegmentEncryptionIV != null) { +// segmentEncryptionIV = fullSegmentEncryptionIV; +// } else { +// segmentEncryptionIV = Long.toHexString(segmentMediaSequence); +// } +// +// segmentMediaSequence++; +// if (segmentByteRangeLength == C.LENGTH_UNSET) { +// segmentByteRangeOffset = 0; +// } +// +// if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) { +// DrmInitData.SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new DrmInitData.SchemeData[0]); +// cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas); +// if (playlistProtectionSchemes == null) { +// DrmInitData.SchemeData[] playlistSchemeDatas = new DrmInitData.SchemeData[schemeDatas.length]; +// for (int i = 0; i < schemeDatas.length; i++) { +// playlistSchemeDatas[i] = schemeDatas[i].copyWithData(null); +// } +// playlistProtectionSchemes = new DrmInitData(encryptionScheme, playlistSchemeDatas); +// } +// } +// +// line = URLEncoder.encode(line, "utf-8"); +// +// Log.i("MediaService", "Player : Parser : Line 3: " + replaceVariableReferences(line, variableDefinitions)); +// +// segments.add( +// new HlsMediaPlaylist.Segment( +// replaceVariableReferences(line, variableDefinitions), +// initializationSegment, +// segmentTitle, +// segmentDurationUs, +// relativeDiscontinuitySequence, +// segmentStartTimeUs, +// cachedDrmInitData, +// fullSegmentEncryptionKeyUri, +// segmentEncryptionIV, +// segmentByteRangeOffset, +// segmentByteRangeLength, +// hasGapTag, +// updatedParts)); +// segmentStartTimeUs += segmentDurationUs; +// segmentDurationUs = 0; +// segmentTitle = ""; +// if (segmentByteRangeLength != C.LENGTH_UNSET) { +// segmentByteRangeOffset += segmentByteRangeLength; +// } +// segmentByteRangeLength = C.LENGTH_UNSET; +// hasGapTag = false; +// } +// } +// +// return new HlsMediaPlaylist( +// playlistType, +// baseUri, +// tags, +// startOffsetUs, +// false, +// playlistStartTimeUs, +// hasDiscontinuitySequence, +// playlistDiscontinuitySequence, +// mediaSequence, +// version, +// targetDurationUs, +// targetDurationUs, +// hasIndependentSegmentsTag, +// hasEndTag, +// /* hasProgramDateTime= */ playlistStartTimeUs != 0, +// playlistProtectionSchemes, +// segments, trailingParts, serverControl, renditionReports); +// } +// +// @C.SelectionFlags +// private static int parseSelectionFlags(String line) { +// int flags = 0; +// if (parseOptionalBooleanAttribute(line, REGEX_DEFAULT, false)) { +// flags |= C.SELECTION_FLAG_DEFAULT; +// } +// if (parseOptionalBooleanAttribute(line, REGEX_FORCED, false)) { +// flags |= C.SELECTION_FLAG_FORCED; +// } +// if (parseOptionalBooleanAttribute(line, REGEX_AUTOSELECT, false)) { +// flags |= C.SELECTION_FLAG_AUTOSELECT; +// } +// return flags; +// } +// +// @C.RoleFlags +// private static int parseRoleFlags(String line, Map variableDefinitions) { +// String concatenatedCharacteristics = +// parseOptionalStringAttr(line, REGEX_CHARACTERISTICS, variableDefinitions); +// if (TextUtils.isEmpty(concatenatedCharacteristics)) { +// return 0; +// } +// String[] characteristics = Util.split(concatenatedCharacteristics, ","); +// @C.RoleFlags int roleFlags = 0; +// if (Util.contains(characteristics, "public.accessibility.describes-video")) { +// roleFlags |= C.ROLE_FLAG_DESCRIBES_VIDEO; +// } +// if (Util.contains(characteristics, "public.accessibility.transcribes-spoken-dialog")) { +// roleFlags |= C.ROLE_FLAG_TRANSCRIBES_DIALOG; +// } +// if (Util.contains(characteristics, "public.accessibility.describes-music-and-sound")) { +// roleFlags |= C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; +// } +// if (Util.contains(characteristics, "public.easy-to-read")) { +// roleFlags |= C.ROLE_FLAG_EASY_TO_READ; +// } +// return roleFlags; +// } +// +// private static int parseChannelsAttribute(String line, Map variableDefinitions) { +// String channelsString = parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions); +// return channelsString != null +// ? Integer.parseInt(Util.splitAtFirst(channelsString, "/")[0]) +// : Format.NO_VALUE; +// } +// +// @Nullable +// private static DrmInitData.SchemeData parseDrmSchemeData( +// String line, String keyFormat, Map variableDefinitions) +// throws ParserException { +// String keyFormatVersions = +// parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions); +// if (KEYFORMAT_WIDEVINE_PSSH_BINARY.equals(keyFormat)) { +// String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions); +// return new DrmInitData.SchemeData( +// C.WIDEVINE_UUID, +// MimeTypes.VIDEO_MP4, +// Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT)); +// } else if (KEYFORMAT_WIDEVINE_PSSH_JSON.equals(keyFormat)) { +// return new DrmInitData.SchemeData(C.WIDEVINE_UUID, "hls", Util.getUtf8Bytes(line)); +// } else if (KEYFORMAT_PLAYREADY.equals(keyFormat) && "1".equals(keyFormatVersions)) { +// String uriString = parseStringAttr(line, REGEX_URI, variableDefinitions); +// byte[] data = Base64.decode(uriString.substring(uriString.indexOf(',')), Base64.DEFAULT); +// byte[] psshData = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, data); +// return new DrmInitData.SchemeData(C.PLAYREADY_UUID, MimeTypes.VIDEO_MP4, psshData); +// } +// 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 { +// String value = parseOptionalStringAttr(line, pattern, variableDefinitions); +// if (value != null) { +// return value; +// } else { +// throw ParserException.createForUnsupportedContainerFeature("Couldn't match " + pattern.pattern() + " in " + line); +// } +// } +// +// private static @Nullable +// String parseOptionalStringAttr( +// String line, Pattern pattern, Map variableDefinitions) { +// return parseOptionalStringAttr(line, pattern, null, variableDefinitions); +// } +// +// private static String parseOptionalStringAttr( +// String line, +// Pattern pattern, +// String defaultValue, +// Map variableDefinitions) { +// Matcher matcher = pattern.matcher(line); +// String value = matcher.find() ? matcher.group(1) : defaultValue; +// return variableDefinitions.isEmpty() || value == null +// ? value +// : replaceVariableReferences(value, variableDefinitions); +// } +// +// private static String replaceVariableReferences( +// String string, Map variableDefinitions) { +// Matcher matcher = REGEX_VARIABLE_REFERENCE.matcher(string); +// // TODO: Replace StringBuffer with StringBuilder once Java 9 is available. +// StringBuffer stringWithReplacements = new StringBuffer(); +// while (matcher.find()) { +// String groupName = matcher.group(1); +// if (variableDefinitions.containsKey(groupName)) { +// matcher.appendReplacement( +// stringWithReplacements, Matcher.quoteReplacement(variableDefinitions.get(groupName))); +// } else { +// // The variable is not defined. The value is ignored. +// } +// } +// matcher.appendTail(stringWithReplacements); +// return stringWithReplacements.toString(); +// } +// +// private static boolean parseOptionalBooleanAttribute( +// String line, Pattern pattern, boolean defaultValue) { +// Matcher matcher = pattern.matcher(line); +// if (matcher.find()) { +// return matcher.group(1).equals(BOOLEAN_TRUE); +// } +// return defaultValue; +// } +// +// private static Pattern compileBooleanAttrPattern(String attribute) { +// return Pattern.compile(attribute + "=(" + BOOLEAN_FALSE + "|" + BOOLEAN_TRUE + ")"); +// } +// +// private static class LineIterator { +// +// private final BufferedReader reader; +// private final Queue extraLines; +// +// private String next; +// +// public LineIterator(Queue extraLines, BufferedReader reader) { +// this.extraLines = extraLines; +// this.reader = reader; +// } +// +// public boolean hasNext() throws IOException { +// if (next != null) { +// return true; +// } +// if (!extraLines.isEmpty()) { +// next = extraLines.poll(); +// return true; +// } +// while ((next = reader.readLine()) != null) { +// next = next.trim(); +// if (!next.isEmpty()) { +// return true; +// } +// } +// return false; +// } +// +// public String next() throws IOException { +// String result = null; +// if (hasNext()) { +// result = next; +// next = null; +// } +// return result; +// } +// +// } +// +//} \ No newline at end of file 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..f0277cc0 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,45 +1,45 @@ -package br.com.suamusica.player.media.parser; - -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; - -public final class SMHlsPlaylistParserFactory implements HlsPlaylistParserFactory { - - private final List streamKeys; - - /** Creates an instance that does not filter any parsing results. */ - public SMHlsPlaylistParserFactory() { - this(Collections.emptyList()); - } - - /** - * Creates an instance that filters the parsing results using the given {@code streamKeys}. - * - * @param streamKeys See {@link - * FilteringManifestParser#FilteringManifestParser(ParsingLoadable.Parser, List)}. - */ - public SMHlsPlaylistParserFactory(List streamKeys) { - this.streamKeys = streamKeys; - } - - @Override - public ParsingLoadable.Parser createPlaylistParser() { - return new FilteringManifestParser<>(new CustomHlsPlaylistParser(), streamKeys); - } - - @Override - public ParsingLoadable.Parser createPlaylistParser(HlsMultivariantPlaylist multivariantPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist) { - return new FilteringManifestParser<>(new CustomHlsPlaylistParser(), streamKeys); - } - -} +//package br.com.suamusica.player.media.parser; +// +//import androidx.annotation.Nullable; +// +//import java.util.Collections; +//import java.util.List; +// +//import com.google.android.exoplayer2.offline.FilteringManifestParser; +//import androidx.media3.common.StreamKey; +//import com.google.media3.common.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; +// +//public final class SMHlsPlaylistParserFactory implements HlsPlaylistParserFactory { +// +// private final List streamKeys; +// +// /** Creates an instance that does not filter any parsing results. */ +// public SMHlsPlaylistParserFactory() { +// this(Collections.emptyList()); +// } +// +// /** +// * Creates an instance that filters the parsing results using the given {@code streamKeys}. +// * +// * @param streamKeys See {@link +// * FilteringManifestParser#FilteringManifestParser(ParsingLoadable.Parser, List)}. +// */ +// public SMHlsPlaylistParserFactory(List streamKeys) { +// this.streamKeys = streamKeys; +// } +// +// @Override +// public ParsingLoadable.Parser createPlaylistParser() { +// return new FilteringManifestParser<>(new CustomHlsPlaylistParser(), streamKeys); +// } +// +// @Override +// public ParsingLoadable.Parser createPlaylistParser(HlsMultivariantPlaylist multivariantPlaylist, @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 de8a34aa..83be2397 100644 --- a/packages/player/example/.flutter-plugins-dependencies +++ b/packages/player/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"isar_flutter_libs","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/suamusica/projects/suamusica/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"android":[{"name":"isar_flutter_libs","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_android-2.2.2/","native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/suamusica/projects/suamusica/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"macos":[{"name":"isar_flutter_libs","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"isar_flutter_libs","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"isar_flutter_libs","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/","native_build":false,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"isar_flutter_libs","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider"]}],"date_created":"2024-02-05 17:03:28.400968","version":"3.19.0-0.3.pre"} \ 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"> - - - - \ 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 cb980bbf..00000000 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaControlBroadcastReceiver.kt +++ /dev/null @@ -1,13 +0,0 @@ -package br.com.suamusica.player - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import androidx.media3.common.util.UnstableApi - -class MediaControlBroadcastReceiver : BroadcastReceiver() { - @UnstableApi - override fun onReceive(context: Context?, intent: Intent?) { - MediaButtonEventHandler(null).onMediaButtonEventHandler(intent) - } -} \ No newline at end of file From 968c43cb006c04e1976bf71f97e1d20421000e8a Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 22 Aug 2024 17:40:16 -0300 Subject: [PATCH 12/70] remove comments and fix release --- .../player/MediaButtonEventHandler.kt | 1 - .../br/com/suamusica/player/MediaService.kt | 101 +----------------- 2 files changed, 4 insertions(+), 98 deletions(-) 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 54c4bb5f..f50bc7c4 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 @@ -72,7 +72,6 @@ class MediaButtonEventHandler( customCommand: SessionCommand, args: Bundle ): ListenableFuture { - Log.d("Player", "TESTE1 CUSTOM_COMMAND: ${customCommand.customAction} | $args") if (customCommand.customAction == "favoritar" || customCommand.customAction == "desfavoritar") { val isFavorite = customCommand.customAction == "favoritar" buildIcons(isFavorite) 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 a89896f3..7ab48791 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 @@ -11,9 +11,6 @@ import android.os.Build import android.os.Bundle import android.os.Handler import android.os.PowerManager -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.media3.common.AudioAttributes @@ -183,17 +180,15 @@ class MediaService : MediaSessionService() { } override fun onDestroy() { + removeNotification() Log.d(TAG, "onDestroy") -// mediaController?.unregisterCallback(mediaControllerCallback) releaseLock() - removeNotification() player?.release() stopSelf() mediaSession?.run { - player.release() - release() - mediaSession = null + releaseAndPerformAndDisableTracking() + Log.d("MusicService", "onDestroy") } releasePossibleLeaks() @@ -233,7 +228,6 @@ class MediaService : MediaSessionService() { dataSourceFactory.setUserAgent(userAgent) dataSourceFactory.setAllowCrossProtocolRedirects(true) dataSourceFactory.setDefaultRequestProperties(mapOf("Cookie" to cookie)) - // Metadata Build val metadata = buildMetaData(media) val url = media.url Log.i(TAG, "Player: URL: $url") @@ -341,7 +335,7 @@ class MediaService : MediaSessionService() { } } - fun release() { + fun releaseAndPerformAndDisableTracking() { performAndDisableTracking { player?.stop() } @@ -513,25 +507,6 @@ class MediaService : MediaSessionService() { } } -// fun shouldStartService() { -// 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 -// -// } -// } - private fun stopService() { if (isForegroundService) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -544,72 +519,4 @@ class MediaService : MediaSessionService() { 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, "onPlaybackStateChanged1 state: $state") -// updateNotification(state!!) - } - - override fun onQueueChanged(queue: MutableList?) { - Log.d(TAG, "onQueueChanged queue: $queue") - } - -// @SuppressLint("WakelockTimeout") -// private fun updateNotification(state: PlaybackStateCompat) { -// Log.d(TAG, "TESTE1 updateNotification") -// if (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() -// } -// } -// } -// } -// } - } } From 35491ba0aa961ba441eb7e030fafae13d787901b Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 26 Aug 2024 09:44:01 -0300 Subject: [PATCH 13/70] fix remove notification --- .../player/MediaButtonEventHandler.kt | 72 +++---- .../br/com/suamusica/player/MediaService.kt | 184 +++++++++++------- .../com/suamusica/player/PlayerSingleton.kt | 1 + 3 files changed, 152 insertions(+), 105 deletions(-) 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 f50bc7c4..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 @@ -5,12 +5,15 @@ import android.os.Build import android.os.Bundle import android.util.Log import android.view.KeyEvent +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 @@ -32,20 +35,23 @@ class MediaButtonEventHandler( ): MediaSession.ConnectionResult { Log.d("Player", "onConnect") val sessionCommands = - MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() - .add(SessionCommand("next", Bundle.EMPTY)) - .add(SessionCommand("seek", session.token.extras)) - .add(SessionCommand("previous", Bundle.EMPTY)) - .add(SessionCommand("pause", Bundle.EMPTY)) - .add(SessionCommand("favoritar", Bundle.EMPTY)) - .add(SessionCommand("desfavoritar", 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() + 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() val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() @@ -61,19 +67,15 @@ class MediaButtonEventHandler( .build() } - override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { - super.onPostConnect(session, controller) - Log.d("Player", "onPostConnect") - } - override fun onCustomCommand( session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle ): ListenableFuture { - if (customCommand.customAction == "favoritar" || customCommand.customAction == "desfavoritar") { - val isFavorite = customCommand.customAction == "favoritar" + 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) } @@ -86,6 +88,10 @@ class MediaButtonEventHandler( mediaService?.togglePlayPause() } + if (customCommand.customAction == "stop") { + mediaService?.stop() + } + if (customCommand.customAction == "send_notification") { args.let { val isFavorite = it.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT) @@ -96,17 +102,17 @@ class MediaButtonEventHandler( if (customCommand.customAction == "play") { mediaService?.play() } - if (customCommand.customAction == "previous") { + if (customCommand.customAction == "notification_previous") { PlayerSingleton.previous() } - if (customCommand.customAction == "next") { + if (customCommand.customAction == "notification_next") { PlayerSingleton.next() } if (customCommand.customAction == "pause") { mediaService?.pause() } - if (customCommand.customAction == "remove_notification" || customCommand.customAction == "ads_playing" ) { - mediaService?.removeNotification(); + if (customCommand.customAction == "ads_playing" || customCommand.customAction == "remove_notification") { + mediaService?.removeNotification() } if (customCommand.customAction == "prepare") { @@ -140,29 +146,29 @@ class MediaButtonEventHandler( .setIconResId(if (isFavorite) drawable.media3_icon_heart_filled else drawable.media3_icon_heart_unfilled) .setSessionCommand( SessionCommand( - if (isFavorite) "desfavoritar" else "favoritar", + if (isFavorite) "notification_desfavoritar" else "notification_favoritar", Bundle() ) ) .setEnabled(true) .build(), CommandButton.Builder() - .setDisplayName("next") + .setDisplayName("notification_next") .setIconResId(drawable.media3_icon_next) - .setSessionCommand(SessionCommand("next", Bundle.EMPTY)) + .setSessionCommand(SessionCommand("notification_next", Bundle.EMPTY)) .setEnabled(true) .build(), ) return mediaService?.mediaSession?.setCustomLayout( - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { list.plus( CommandButton.Builder() - .setDisplayName("previous") + .setDisplayName("notification_previous") .setIconResId(drawable.media3_icon_previous) - .setSessionCommand(SessionCommand("previous", Bundle.EMPTY)) + .setSessionCommand(SessionCommand("notification_previous", Bundle.EMPTY)) .build() ) - } else{ + } else { list } ) @@ -202,7 +208,7 @@ class MediaButtonEventHandler( } Log.d("Player", "Key: $ke") - + Log.d("Player", "#MEDIA3# - Key: $ke") when (ke.keyCode) { KeyEvent.KEYCODE_MEDIA_PLAY -> { PlayerSingleton.play() diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt index 7ab48791..95c79256 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,5 +1,6 @@ package br.com.suamusica.player +import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent @@ -15,6 +16,7 @@ import android.support.v4.media.session.PlaybackStateCompat import android.util.Log 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 @@ -34,20 +36,15 @@ 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.common.collect.ImmutableList import com.google.common.util.concurrent.ListenableFuture -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import java.io.File import java.util.concurrent.TimeUnit @@ -77,49 +74,50 @@ class MediaService : MediaSessionService() { private var previousState: Int = -1 private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader + private lateinit var mediaButtonEventHandler: MediaButtonEventHandler - 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) - lateinit var bitmap: Bitmap - 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 { - BitmapFactory.decodeResource(context.resources, R.drawable.default_art) - } - } - } - withContext(Dispatchers.Main) { - callback(bitmap) - } - } - } - } +// 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) +// lateinit var bitmap: Bitmap +// 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 { +// BitmapFactory.decodeResource(context.resources, R.drawable.default_art) +// } +// } +// } +// withContext(Dispatchers.Main) { +// callback(bitmap) +// } +// } +// } +// } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "onStartCommand") @@ -130,6 +128,7 @@ class MediaService : MediaSessionService() { override fun onCreate() { super.onCreate() + mediaButtonEventHandler = MediaButtonEventHandler(this) packageValidator = PackageValidator(applicationContext, R.xml.allowed_media_browser_callers) wifiLock = (applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager).createWifiLock( @@ -152,11 +151,63 @@ class MediaService : MediaSessionService() { dataSourceBitmapLoader = DataSourceBitmapLoader(applicationContext) + Log.d(TAG, "MEDIA3 - handleCustomCommand ${Build.VERSION_CODES.TIRAMISU}") player?.let { mediaSession = MediaSession.Builder(this, it) .setBitmapLoader(CacheBitmapLoader(dataSourceBitmapLoader)) - .setCallback(MediaButtonEventHandler(this)) + .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 + } + val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" + 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 +// channelId = NOW_PLAYING_NOTIFICATION + }) + } + + override fun handleCustomCommand( + session: MediaSession, + action: String, + extras: Bundle + ): Boolean { + Log.d(TAG, "#MEDIA3# - handleCustomCommand $action") + return false + } + }) } } @@ -276,12 +327,7 @@ class MediaService : MediaSessionService() { dataSourceBitmapLoader.loadBitmap(Uri.parse(media.bigCoverUrl!!)) .get(5000, TimeUnit.MILLISECONDS) } catch (e: Exception) { - Log.d("Player", "TESTE1 catch") - var bitmapFallback: Bitmap? = null - getArts(applicationContext, media.bigCoverUrl) { bitmap -> - bitmapFallback = bitmap - } - bitmapFallback + BitmapFactory.decodeResource(resources, R.drawable.default_art) } art?.compress(Bitmap.CompressFormat.PNG, 95, stream) @@ -297,15 +343,18 @@ class MediaService : MediaSessionService() { return metadata } - // } fun play() { performAndEnableTracking { player?.play() } } - + fun removeNotification() { - player?.removeMediaItem(0); + object: ForwardingPlayer(player!!) { + override fun getDuration(): Long { + return C.TIME_UNSET + } + } } fun seek(position: Long, playWhenReady: Boolean) { @@ -335,20 +384,12 @@ class MediaService : MediaSessionService() { } } - fun releaseAndPerformAndDisableTracking() { + 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 @@ -443,8 +484,7 @@ class MediaService : MediaSessionService() { } else { stopTrackingProgressAndPerformTask {} } - } - else if(playbackState == Player.STATE_ENDED) { + } else if (playbackState == Player.STATE_ENDED) { stopTrackingProgressAndPerformTask {} } previousState = playbackState 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 9cf70b81..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 @@ -48,6 +48,7 @@ object PlayerSingleton { } fun next() { + Log.d("Player", "#MEDIA3# - commandCenter NEXT") channel?.invokeMethod("commandCenter.onNext", emptyMap()) } From e3c7c874af9a0ba89ff7f071d0e5790e71f9327c Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 26 Aug 2024 11:58:05 -0300 Subject: [PATCH 14/70] remove unused code --- .../br/com/suamusica/player/MediaService.kt | 46 +------------------ 1 file changed, 1 insertion(+), 45 deletions(-) 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 95c79256..1ade6b97 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 @@ -76,49 +76,6 @@ class MediaService : MediaSessionService() { private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader private lateinit var mediaButtonEventHandler: MediaButtonEventHandler -// 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) -// lateinit var bitmap: Bitmap -// 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 { -// BitmapFactory.decodeResource(context.resources, R.drawable.default_art) -// } -// } -// } -// withContext(Dispatchers.Main) { -// callback(bitmap) -// } -// } -// } -// } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "onStartCommand") super.onStartCommand(intent, flags, startId) @@ -178,12 +135,12 @@ class MediaService : MediaSessionService() { 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 } - val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" val NOW_PLAYING_NOTIFICATION = 0xb339 val notifyPendingIntent = PendingIntent.getActivity( applicationContext, @@ -195,7 +152,6 @@ class MediaService : MediaSessionService() { NOW_PLAYING_NOTIFICATION, customMedia3Notification.notification.apply { contentIntent = notifyPendingIntent -// channelId = NOW_PLAYING_NOTIFICATION }) } From 2fbc084f69bd7a4e5aacb05815954d3f393a06b2 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 28 Aug 2024 11:42:58 -0300 Subject: [PATCH 15/70] wip --- .../android/src/main/AndroidManifest.xml | 2 +- .../player/MediaButtonEventHandler.kt | 23 +- .../br/com/suamusica/player/MediaService.kt | 329 ++++++++++++------ .../android/src/main/res/values/strings.xml | 1 + .../example/.flutter-plugins-dependencies | 135 +------ 5 files changed, 240 insertions(+), 250 deletions(-) diff --git a/packages/player/android/src/main/AndroidManifest.xml b/packages/player/android/src/main/AndroidManifest.xml index 57ec7ce7..9fd545b9 100644 --- a/packages/player/android/src/main/AndroidManifest.xml +++ b/packages/player/android/src/main/AndroidManifest.xml @@ -11,8 +11,8 @@ - + 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 43dc15b1..fb1a0774 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 @@ -25,8 +25,8 @@ import com.google.common.util.concurrent.ListenableFuture @UnstableApi class MediaButtonEventHandler( - private val mediaService: MediaService?, -) : MediaLibraryService.MediaLibrarySession.Callback { + private val mediaService: MediaService, +) : MediaSession.Callback { private val BROWSABLE_ROOT = "/" private val EMPTY_ROOT = "@empty@" override fun onConnect( @@ -81,15 +81,15 @@ class MediaButtonEventHandler( } if (customCommand.customAction == "seek") { - mediaService?.seek(args.getLong("position"), args.getBoolean("playWhenReady")) + mediaService.seek(args.getLong("position"), args.getBoolean("playWhenReady")) } if (customCommand.customAction == "onTogglePlayPause") { - mediaService?.togglePlayPause() + mediaService.togglePlayPause() } if (customCommand.customAction == "stop") { - mediaService?.stop() + mediaService.stop() } if (customCommand.customAction == "send_notification") { @@ -100,7 +100,7 @@ class MediaButtonEventHandler( } } if (customCommand.customAction == "play") { - mediaService?.play() + mediaService.play() } if (customCommand.customAction == "notification_previous") { PlayerSingleton.previous() @@ -109,10 +109,11 @@ class MediaButtonEventHandler( PlayerSingleton.next() } if (customCommand.customAction == "pause") { - mediaService?.pause() + mediaService.pause() } if (customCommand.customAction == "ads_playing" || customCommand.customAction == "remove_notification") { - mediaService?.removeNotification() +// mediaService.adsPlaying() + mediaService.removeNotification() } if (customCommand.customAction == "prepare") { @@ -127,7 +128,7 @@ class MediaButtonEventHandler( if (it.containsKey(PlayerPlugin.IS_FAVORITE_ARGUMENT)) { isFavorite = it.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT) } - mediaService?.prepare( + mediaService.prepare( cookie, Media(name, author, url, coverUrl, bigCoverUrl, isFavorite) ) @@ -159,7 +160,7 @@ class MediaButtonEventHandler( .setEnabled(true) .build(), ) - return mediaService?.mediaSession?.setCustomLayout( + return mediaService.mediaSession.setCustomLayout( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { list.plus( CommandButton.Builder() @@ -220,7 +221,7 @@ class MediaButtonEventHandler( KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { Log.d("Player", "Player: Key Code : PlayPause") - mediaService?.togglePlayPause() + mediaService.togglePlayPause() } KeyEvent.KEYCODE_MEDIA_NEXT -> { 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 79ae3e3a..a6dae0d9 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,5 +1,6 @@ package br.com.suamusica.player +import android.app.ActivityManager import android.app.PendingIntent import android.app.Service import android.content.Context @@ -16,7 +17,6 @@ import android.support.v4.media.session.PlaybackStateCompat import android.util.Log 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 @@ -38,32 +38,37 @@ 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.DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID import androidx.media3.session.MediaController import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService +import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionResult import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory 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 -import java.io.IOException import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean + const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" + const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 + @UnstableApi 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 packageValidator: PackageValidator? = null private var media: Media? = null private var isForegroundService = false - private var wifiLock: WifiManager.WifiLock? = null - private var wakeLock: PowerManager.WakeLock? = null - var mediaSession: MediaSession? = null + + // private var wifiLock: WifiManager.WifiLock? = null +// private var wakeLock: PowerManager.WakeLock? = null + lateinit var mediaSession: MediaSession private var mediaController: ListenableFuture? = null private val uAmpAudioAttributes = @@ -78,60 +83,84 @@ class MediaService : MediaSessionService() { private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader private lateinit var mediaButtonEventHandler: MediaButtonEventHandler + private var customMedia3Notification: MediaNotification? = null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "onStartCommand") super.onStartCommand(intent, flags, startId) return Service.START_STICKY + } + private fun getPendingIntent(): PendingIntent { + val notifyIntent = Intent("SUA_MUSICA_FLUTTER_NOTIFICATION_CLICK").apply { + addCategory(Intent.CATEGORY_APP_MUSIC) + flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + } + + return PendingIntent.getActivity( + applicationContext, + 0, + notifyIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT else PendingIntent.FLAG_UPDATE_CURRENT + ) } override fun onCreate() { super.onCreate() mediaButtonEventHandler = MediaButtonEventHandler(this) - packageValidator = PackageValidator(applicationContext, R.xml.allowed_media_browser_callers) - 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) +// packageValidator = PackageValidator(applicationContext, R.xml.allowed_media_browser_callers) +// 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) player = ExoPlayer.Builder(this).build().apply { setAudioAttributes(uAmpAudioAttributes, true) addListener(playerEventListener()) - // setWakeMode(C.WAKE_MODE_NETWORK) + setWakeMode(C.WAKE_MODE_NETWORK) setHandleAudioBecomingNoisy(true) } dataSourceBitmapLoader = DataSourceBitmapLoader(applicationContext) + Log.d(TAG, "MEDIA3 - handleCustomCommand ${Build.VERSION_CODES.TIRAMISU}") player?.let { mediaSession = MediaSession.Builder(this, it) .setBitmapLoader(CacheBitmapLoader(dataSourceBitmapLoader)) .setCallback(mediaButtonEventHandler) + .setSessionActivity(getPendingIntent()) + .setId("SM_NOW_PLAYING") .build() - this@MediaService.setMediaNotificationProvider(object : MediaNotification.Provider { + override fun createNotification( mediaSession: MediaSession, customLayout: ImmutableList, actionFactory: MediaNotification.ActionFactory, onNotificationChangedCallback: MediaNotification.Provider.Callback ): MediaNotification { + Log.d(TAG, "#MEDIA3# - createNotification | ${mediaSession.id}") val defaultMediaNotificationProvider = - DefaultMediaNotificationProvider(applicationContext) - .apply { + DefaultMediaNotificationProvider(applicationContext, +// DefaultMediaNotificationProvider( +// applicationContext, +// { R.string.notification_id }, +// NOW_PLAYING_CHANNEL, +// R.string.notification_channel + ).apply { setSmallIcon(R.drawable.ic_notification) } - val customMedia3Notification = + customMedia3Notification = defaultMediaNotificationProvider.createNotification( mediaSession, mediaSession.customLayout, @@ -139,23 +168,10 @@ class MediaService : MediaSessionService() { 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 - } - 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 - }) + customMedia3Notification!!.notification + ) } override fun handleCustomCommand( @@ -172,62 +188,87 @@ class MediaService : MediaSessionService() { override fun onGetSession( controllerInfo: MediaSession.ControllerInfo - ): MediaSession? = mediaSession + ): MediaSession = mediaSession override fun onTaskRemoved(rootIntent: Intent?) { - Log.d(TAG, "onTaskRemoved") - super.onTaskRemoved(rootIntent) - - /** - * By stopping playback, the player will transition to [Player.STATE_IDLE]. This will - * cause a state change in the MediaSession, and (most importantly) call - * [MediaControllerCallback.onPlaybackStateChanged]. Because the playback state will - * be reported as [PlaybackStateCompat.STATE_NONE], the service will first remove - * itself as a foreground service, and will then call [stopSelf]. - */ - player?.stop() - stopService() +// val player = mediaSession?.player!! +// val a = !player.playWhenReady +// || player.mediaItemCount == 0 +// || player.playbackState == Player.STATE_ENDED +// Log.d(TAG, "#MEDIA3# - onTaskRemoved $a") +// if (a) { +// // Stop the service if not playing, continue playing in the background +// // otherwise. +// stopSelf() +// } + isServiceRunning() } override fun onDestroy() { - removeNotification() - Log.d(TAG, "onDestroy") - releaseLock() - player?.release() - stopSelf() - mediaSession?.run { releaseAndPerformAndDisableTracking() - Log.d("MusicService", "onDestroy") + player.release() + release() + mediaSession.release() } - + releaseLock() releasePossibleLeaks() + stopSelf() super.onDestroy() - } private fun releasePossibleLeaks() { player?.release() - packageValidator = null - mediaSession = null +// packageValidator = null + mediaSession.release() mediaController = null - wifiLock = null - wakeLock = null - +// wifiLock = null +// wakeLock = null } private fun acquireLock(duration: Long) { - wifiLock?.acquire() - wakeLock?.acquire(duration) +// wifiLock?.acquire() +// wakeLock?.acquire(duration) } - private fun releaseLock() { - try { - if (wifiLock?.isHeld == true) wifiLock?.release() - if (wakeLock?.isHeld == true) wakeLock?.release() - } catch (e: Exception) { - Log.e("MusicService", e.message, e) + + private fun isServiceRunning(): Boolean { + val manager = getSystemService(ACTIVITY_SERVICE) as ActivityManager + for (service in manager.getRunningServices(Int.MAX_VALUE)) { + if ("br.com.suamusica.player.MediaService" == service.service.className) { + return true + } } + return false + } + + fun shouldStartService() { +// Log.e(TAG, "#MEDIA3# shouldStartService $isForegroundService | ${customMedia3Notification == null}") +// val intent = Intent(applicationContext, this@MediaService.javaClass) +// if (!isForegroundService) { +// if (Build.VERSION.SDK_INT >= 26) { +// applicationContext.startForegroundService(intent) +//// setForegroundServiceBehavior() +//// startService(intent) +// } else { +// applicationContext.startService(intent) +// } +// if(customMedia3Notification!=null){ +// startForeground(NOW_PLAYING_NOTIFICATION, customMedia3Notification!!.notification) +// } +// isForegroundService = true +// } + val a = isServiceRunning() + Log.e(TAG, "#MEDIA3# isServiceRunning $a") + } + + private fun releaseLock() { +// try { +// if (wifiLock?.isHeld == true) wifiLock?.release() +// if (wakeLock?.isHeld == true) wakeLock?.release() +// } catch (e: Exception) { +// Log.e("MusicService", e.message, e) +// } } fun prepare(cookie: String, media: Media) { @@ -268,14 +309,16 @@ class MediaService : MediaSessionService() { } } player?.pause() - player?.prepare(source) + player?.setMediaSource(source) +// player?.addMediaSource(source) + player?.prepare() } private fun buildMetaData(media: Media): MediaMetadata { val metadataBuilder = MediaMetadata.Builder() if (media.isFavorite != null) { - mediaSession?.sessionExtras?.putBoolean( + mediaSession.sessionExtras.putBoolean( PlayerPlugin.IS_FAVORITE_ARGUMENT, media.isFavorite ) @@ -307,13 +350,23 @@ class MediaService : MediaSessionService() { player?.play() } } - + + // fun adsPlaying() { +// val oldItem = player!!.currentMediaItem!! +// val newItem = oldItem +// .buildUpon().setMediaMetadata( +// oldItem.mediaMetadata.buildUpon() +// .setTitle("Propaganda") +// .setDescription("Propaganda") +// .build() +// ) +// .build() +// player!!.replaceMediaItem(0, newItem) +// player!!.prepare() +// } fun removeNotification() { - object: ForwardingPlayer(player!!) { - override fun getDuration(): Long { - return C.TIME_UNSET - } - } +// player?.stop() +// shouldStartService() } fun seek(position: Long, playWhenReady: Boolean) { @@ -334,12 +387,10 @@ class MediaService : MediaSessionService() { } fun togglePlayPause() { - performAndDisableTracking { - if (player?.isPlaying == true) { - player?.pause() - } else { - player?.play() - } + if (player?.isPlaying == true) { + pause() + } else { + play() } } @@ -421,30 +472,100 @@ class MediaService : MediaSessionService() { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) if (isPlaying) { + shouldStartService() + } +// shouldStartService() +// val duration = player?.duration ?: 0L +// acquireLock( +// if (duration > 1L) duration + TimeUnit.MINUTES.toMillis(2) else TimeUnit.MINUTES.toMillis( +// 3 +// ) +// ) +// } else { +// stopService() +// releaseLock() +// } + } + + // +// 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 { +// stopTrackingProgressAndPerformTask {} +// } +// } else if (playbackState == Player.STATE_ENDED) { +// stopTrackingProgressAndPerformTask {} +// } +// previousState = playbackState +// } + override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { + isServiceRunning() + Log.i( + TAG, + "onPlayerStateChanged: playWhenReady: $playWhenReady playbackState: $playbackState currentPlaybackState: ${player?.playbackState} ServiceRunning: ${isServiceRunning()}" + ) + if (playWhenReady) { val duration = player?.duration ?: 0L acquireLock( if (duration > 1L) duration + TimeUnit.MINUTES.toMillis(2) else TimeUnit.MINUTES.toMillis( 3 ) ) - } else - releaseLock() - } + } else { +// releaseLock() + } - 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() + if (playWhenReady && playbackState == ExoPlayer.STATE_READY) { + // + } else { + if (player?.playerError != null) { + // } else { - stopTrackingProgressAndPerformTask {} + 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 { + // + } + } + } } - } else if (playbackState == Player.STATE_ENDED) { - stopTrackingProgressAndPerformTask {} } previousState = playbackState } diff --git a/packages/player/android/src/main/res/values/strings.xml b/packages/player/android/src/main/res/values/strings.xml index 846f8c42..f902887e 100644 --- a/packages/player/android/src/main/res/values/strings.xml +++ b/packages/player/android/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ Media SM - Tocando Agora + SM_NOW_PLAYING Exibe que música está tocando no Sua Música diff --git a/packages/player/example/.flutter-plugins-dependencies b/packages/player/example/.flutter-plugins-dependencies index acf34efb..46a24c2e 100644 --- a/packages/player/example/.flutter-plugins-dependencies +++ b/packages/player/example/.flutter-plugins-dependencies @@ -1,134 +1 @@ -{ - "info": "This is a generated file; do not edit or check into version control.", - "plugins": { - "ios": [ - { - "name": "isar_flutter_libs", - "path": "/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/", - "native_build": true, - "dependencies": [] - }, - { - "name": "path_provider_foundation", - "path": "/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/", - "shared_darwin_source": true, - "native_build": true, - "dependencies": [] - }, - { - "name": "smplayer", - "path": "/Users/suamusica/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 +{"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.4.0/","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.10/","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.4.0/","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.3.0/","native_build":false,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"isar_flutter_libs","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider"]}],"date_created":"2024-08-26 15:15:59.101278","version":"3.24.0","swift_package_manager_enabled":false} \ No newline at end of file From f069104ccd1e9108247af1c804d34c926ef37ece Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 30 Aug 2024 13:34:42 -0300 Subject: [PATCH 16/70] wip --- packages/player/android/build.gradle | 10 +- .../kotlin/br/com/suamusica/player/Media.kt | 1 + .../player/MediaButtonEventHandler.kt | 16 +- .../br/com/suamusica/player/MediaService.kt | 290 +++++------ .../player/MediaSessionConnection.kt | 79 ++- .../suamusica/player/MethodChannelManager.kt | 14 + .../player/MethodChannelManagerArgsBuilder.kt | 24 + .../suamusica/player/PlayerChangeNotifier.kt | 4 + .../br/com/suamusica/player/PlayerPlugin.kt | 148 ++++-- .../com/suamusica/player/PlayerSingleton.kt | 4 +- packages/player/example/lib/sm_player.dart | 4 +- packages/player/lib/player.dart | 3 +- .../player/lib/src/current_queue_updated.dart | 11 + packages/player/lib/src/event.dart | 4 +- packages/player/lib/src/event_type.dart | 1 + packages/player/lib/src/media.dart | 2 + packages/player/lib/src/player.dart | 470 +++++++++--------- packages/player/test/player_test.dart | 84 ++-- 18 files changed, 682 insertions(+), 487 deletions(-) create mode 100644 packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt create mode 100644 packages/player/lib/src/current_queue_updated.dart diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index ab5a5768..cbc1b14b 100644 --- a/packages/player/android/build.gradle +++ b/packages/player/android/build.gradle @@ -50,18 +50,16 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' - 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" + //MEDIA3 DEPENDENCIES + implementation "androidx.media3:media3-exoplayer:$media3_version" implementation "androidx.media3:media3-exoplayer-hls:$media3_version" implementation "androidx.media3:media3-session:$media3_version" implementation "androidx.media3:media3-common:$media3_version" implementation "androidx.media3:media3-ui:$media3_version" + // implementation files('/Users/suamusica/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar') - kapt "com.github.bumptech.glide:compiler:4.12.0" + implementation "com.google.code.gson:gson:2.10.1" } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt index 3b6b2763..99b499d9 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt @@ -6,5 +6,6 @@ class Media( val url: String, val coverUrl: String, val bigCoverUrl: String?, + // val albumId:int? val isFavorite: Boolean? ) \ 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 fb1a0774..329164c2 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 @@ -19,9 +19,12 @@ import androidx.media3.session.MediaSession import androidx.media3.session.R.drawable import androidx.media3.session.SessionCommand import androidx.media3.session.SessionResult +import br.com.suamusica.player.PlayerPlugin.Companion.POSITION_ARGUMENT import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken @UnstableApi class MediaButtonEventHandler( @@ -45,7 +48,10 @@ class MediaButtonEventHandler( add(SessionCommand("seek", session.token.extras)) add(SessionCommand("pause", Bundle.EMPTY)) add(SessionCommand("stop", Bundle.EMPTY)) + add(SessionCommand("next", Bundle.EMPTY)) + add(SessionCommand("enqueue", session.token.extras)) add(SessionCommand("prepare", session.token.extras)) + add(SessionCommand("playFromQueue", session.token.extras)) add(SessionCommand("play", Bundle.EMPTY)) add(SessionCommand("remove_notification", Bundle.EMPTY)) add(SessionCommand("send_notification", session.token.extras)) @@ -102,6 +108,9 @@ class MediaButtonEventHandler( if (customCommand.customAction == "play") { mediaService.play() } + if (customCommand.customAction == "playFromQueue") { + mediaService.playFromQueue(args.getInt(POSITION_ARGUMENT)) + } if (customCommand.customAction == "notification_previous") { PlayerSingleton.previous() } @@ -115,7 +124,12 @@ class MediaButtonEventHandler( // mediaService.adsPlaying() mediaService.removeNotification() } - + if (customCommand.customAction == "enqueue") { + val listType = object : TypeToken?>() {}.type + val yourClassList: List = + Gson().fromJson(args.getString("json"), listType) + mediaService.load(args.getString("cookie")!!,yourClassList,args.getBoolean("autoPlay")) + } if (customCommand.customAction == "prepare") { args.let { val cookie = it.getString("cookie")!! 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 a6dae0d9..eecb956a 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 @@ -34,6 +34,7 @@ import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.FileDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.hls.HlsMediaSource +import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.session.CacheBitmapLoader import androidx.media3.session.CommandButton @@ -53,8 +54,8 @@ import java.io.File import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean - const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" - const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 +const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" +const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 @UnstableApi class MediaService : MediaSessionService() { @@ -62,12 +63,9 @@ class MediaService : MediaSessionService() { private val userAgent = "SuaMusica/player (Linux; Android ${Build.VERSION.SDK_INT}; ${Build.BRAND}/${Build.MODEL})" - // private var packageValidator: PackageValidator? = null private var media: Media? = null private var isForegroundService = false - // private var wifiLock: WifiManager.WifiLock? = null -// private var wakeLock: PowerManager.WakeLock? = null lateinit var mediaSession: MediaSession private var mediaController: ListenableFuture? = null @@ -93,7 +91,7 @@ class MediaService : MediaSessionService() { private fun getPendingIntent(): PendingIntent { val notifyIntent = Intent("SUA_MUSICA_FLUTTER_NOTIFICATION_CLICK").apply { - addCategory(Intent.CATEGORY_APP_MUSIC) + addCategory(Intent.CATEGORY_DEFAULT) flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP } @@ -109,18 +107,7 @@ class MediaService : MediaSessionService() { override fun onCreate() { super.onCreate() mediaButtonEventHandler = MediaButtonEventHandler(this) -// packageValidator = PackageValidator(applicationContext, R.xml.allowed_media_browser_callers) -// 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) + player = ExoPlayer.Builder(this).build().apply { setAudioAttributes(uAmpAudioAttributes, true) @@ -138,7 +125,6 @@ class MediaService : MediaSessionService() { .setBitmapLoader(CacheBitmapLoader(dataSourceBitmapLoader)) .setCallback(mediaButtonEventHandler) .setSessionActivity(getPendingIntent()) - .setId("SM_NOW_PLAYING") .build() this@MediaService.setMediaNotificationProvider(object : MediaNotification.Provider { @@ -150,15 +136,16 @@ class MediaService : MediaSessionService() { ): MediaNotification { Log.d(TAG, "#MEDIA3# - createNotification | ${mediaSession.id}") val defaultMediaNotificationProvider = - DefaultMediaNotificationProvider(applicationContext, + DefaultMediaNotificationProvider( + applicationContext, // DefaultMediaNotificationProvider( // applicationContext, // { R.string.notification_id }, // NOW_PLAYING_CHANNEL, // R.string.notification_channel ).apply { - setSmallIcon(R.drawable.ic_notification) - } + setSmallIcon(R.drawable.ic_notification) + } customMedia3Notification = defaultMediaNotificationProvider.createNotification( @@ -191,27 +178,26 @@ class MediaService : MediaSessionService() { ): MediaSession = mediaSession override fun onTaskRemoved(rootIntent: Intent?) { -// val player = mediaSession?.player!! -// val a = !player.playWhenReady -// || player.mediaItemCount == 0 -// || player.playbackState == Player.STATE_ENDED -// Log.d(TAG, "#MEDIA3# - onTaskRemoved $a") -// if (a) { -// // Stop the service if not playing, continue playing in the background -// // otherwise. -// stopSelf() -// } + val player = mediaSession.player + val shouldStopService = !player.playWhenReady + || player.mediaItemCount == 0 + || player.playbackState == Player.STATE_ENDED + Log.d(TAG, "#MEDIA3# - onTaskRemoved $shouldStopService") + if (shouldStopService) { + // Stop the service if not playing, continue playing in the background + // otherwise. + stopSelf() + } isServiceRunning() } override fun onDestroy() { - mediaSession?.run { + mediaSession.run { releaseAndPerformAndDisableTracking() player.release() release() mediaSession.release() } - releaseLock() releasePossibleLeaks() stopSelf() super.onDestroy() @@ -219,19 +205,10 @@ class MediaService : MediaSessionService() { private fun releasePossibleLeaks() { player?.release() -// packageValidator = null mediaSession.release() mediaController = null -// wifiLock = null -// wakeLock = null } - private fun acquireLock(duration: Long) { -// wifiLock?.acquire() -// wakeLock?.acquire(duration) - } - - private fun isServiceRunning(): Boolean { val manager = getSystemService(ACTIVITY_SERVICE) as ActivityManager for (service in manager.getRunningServices(Int.MAX_VALUE)) { @@ -242,36 +219,21 @@ class MediaService : MediaSessionService() { return false } - fun shouldStartService() { -// Log.e(TAG, "#MEDIA3# shouldStartService $isForegroundService | ${customMedia3Notification == null}") -// val intent = Intent(applicationContext, this@MediaService.javaClass) -// if (!isForegroundService) { -// if (Build.VERSION.SDK_INT >= 26) { -// applicationContext.startForegroundService(intent) -//// setForegroundServiceBehavior() -//// startService(intent) -// } else { -// applicationContext.startService(intent) -// } -// if(customMedia3Notification!=null){ -// startForeground(NOW_PLAYING_NOTIFICATION, customMedia3Notification!!.notification) -// } -// isForegroundService = true -// } - val a = isServiceRunning() - Log.e(TAG, "#MEDIA3# isServiceRunning $a") - } - - private fun releaseLock() { -// try { -// if (wifiLock?.isHeld == true) wifiLock?.release() -// if (wakeLock?.isHeld == true) wakeLock?.release() -// } catch (e: Exception) { -// Log.e("MusicService", e.message, e) -// } + fun load(cookie: String, medias: List, autoPlay: Boolean) { + val sources = listOf() +// player?.clearMediaItems() + medias.forEach { + val a = prepare(cookie, it) + player?.addMediaSource(a) + } + player?.prepare() + PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias) + if (autoPlay) { + play() + } } - fun prepare(cookie: String, media: Media) { + fun prepare(cookie: String, media: Media): MediaSource { this.media = media val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setReadTimeoutMs(15 * 1000) @@ -287,7 +249,7 @@ class MediaService : MediaSessionService() { val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata).build() @C.ContentType val type = Util.inferContentType(uri) Log.i(TAG, "Player: Type: $type HLS: ${C.CONTENT_TYPE_HLS}") - val source = when (type) { + return when (type) { C.CONTENT_TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory) .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) .setAllowChunklessPreparation(true).createMediaSource(mediaItem) @@ -308,10 +270,6 @@ class MediaService : MediaSessionService() { throw IllegalStateException("Unsupported type: $type") } } - player?.pause() - player?.setMediaSource(source) -// player?.addMediaSource(source) - player?.prepare() } private fun buildMetaData(media: Media): MediaMetadata { @@ -346,9 +304,11 @@ class MediaService : MediaSessionService() { } fun play() { - performAndEnableTracking { - player?.play() - } + player?.play() + } + + fun playFromQueue(position: Int) { + player?.seekTo(position, 0) } // fun adsPlaying() { @@ -380,6 +340,20 @@ class MediaService : MediaSessionService() { } } +// fun returnCurrentQueue() { +// try { +// player?.let { +// PlayerSingleton.playerChangeNotifier?.sendCurrentQueue( +// currentQueue(), +// it.currentMediaItemIndex +// ) +// } +// } catch (e: java.lang.Exception) { +// OnePlayerSingleton.log(TAG, "returnCurrentQueue Exception - $e") +// throw e +// } +// } + fun stop() { performAndDisableTracking { player?.stop() @@ -471,9 +445,9 @@ class MediaService : MediaSessionService() { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) - if (isPlaying) { - shouldStartService() - } +// if (isPlaying) { +// shouldStartService() +// } // shouldStartService() // val duration = player?.duration ?: 0L // acquireLock( @@ -488,87 +462,87 @@ class MediaService : MediaSessionService() { } // -// 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 { -// stopTrackingProgressAndPerformTask {} -// } -// } else if (playbackState == Player.STATE_ENDED) { -// stopTrackingProgressAndPerformTask {} -// } -// previousState = playbackState -// } - override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { - isServiceRunning() - Log.i( - TAG, - "onPlayerStateChanged: playWhenReady: $playWhenReady playbackState: $playbackState currentPlaybackState: ${player?.playbackState} ServiceRunning: ${isServiceRunning()}" - ) - if (playWhenReady) { - val duration = player?.duration ?: 0L - acquireLock( - if (duration > 1L) duration + TimeUnit.MINUTES.toMillis(2) else TimeUnit.MINUTES.toMillis( - 3 - ) - ) - } 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 } +// override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { +// isServiceRunning() +// Log.i( +// TAG, +// "onPlayerStateChanged: playWhenReady: $playWhenReady playbackState: $playbackState currentPlaybackState: ${player?.playbackState} ServiceRunning: ${isServiceRunning()}" +// ) +// if (playWhenReady) { +// val duration = player?.duration ?: 0L +//// acquireLock( +//// if (duration > 1L) duration + TimeUnit.MINUTES.toMillis(2) else TimeUnit.MINUTES.toMillis( +//// 3 +//// ) +//// ) +// } else { +//// releaseLock() +// } +// +// if (playWhenReady && playbackState == ExoPlayer.STATE_READY) { +// // +// } else { +// if (player?.playerError != null) { +// // +// } else { +// when (playbackState) { +// ExoPlayer.STATE_IDLE -> { // 1 +// // +// } +// +// +// ExoPlayer.STATE_BUFFERING -> { // 2 +// // +// } +// +// ExoPlayer.STATE_READY -> { // 3 +// val status = +// if (playWhenReady) PlayerState.PLAYING else PlayerState.PAUSED +// if (previousState == -1) { +// // when we define that the track shall not "playWhenReady" +// // no position info is sent +// // therefore, we need to "emulate" the first position notification +// // by sending it directly +// notifyPositionChange() +// } else { +// if (status == PlayerState.PAUSED) { +// stopTrackingProgressAndPerformTask { +// // +// } +// } else { +// // +// } +// +// } +// } +// +// ExoPlayer.STATE_ENDED -> { // 4 +// stopTrackingProgressAndPerformTask { +// // +// } +// } +// } +// } +// } +// previousState = playbackState +// } override fun onRepeatModeChanged(repeatMode: Int) { Log.i(TAG, "onRepeatModeChanged: $repeatMode") 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 fea0cc70..91c4d658 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 @@ -12,8 +12,8 @@ import android.util.Log import java.lang.ref.WeakReference class MediaSessionConnection( - context: Context, - val playerChangeNotifier: PlayerChangeNotifier + context: Context, + val playerChangeNotifier: PlayerChangeNotifier ) { val TAG = "Player" @@ -31,7 +31,8 @@ class MediaSessionConnection( var duration = 0L private val weakContext = WeakReference(context) - private val weakServiceComponent = WeakReference(ComponentName(context, MediaService::class.java)) + private val weakServiceComponent = + WeakReference(ComponentName(context, MediaService::class.java)) private val mediaBrowserConnectionCallback = MediaBrowserConnectionCallback(context) private var mediaBrowser: MediaBrowserCompat? = null @@ -57,13 +58,34 @@ class MediaSessionConnection( sendCommand("prepare", bundle) } - fun play() { - sendCommand("play", null) + fun enqueue(cookie: String, medias: String, autoPlay:Boolean) { + val bundle = Bundle() + bundle.putString("cookie", cookie) + bundle.putString("json", medias) + bundle.putBoolean("autoPlay", autoPlay) + sendCommand("enqueue", bundle) + } + + fun playFromQueue(index: Int) { + val bundle = Bundle() + + bundle.putInt("index", index) + + sendCommand("playFromQueue", bundle) + } + + fun play(index: Int? = null) { + val bundle = Bundle() + if (index != null) { + bundle.putInt("index", index) + } + sendCommand("play", bundle) } fun togglePlayPause() { sendCommand("togglePlayPause", null) } + fun adsPlaying() { sendCommand("ads_playing", null) } @@ -71,8 +93,11 @@ class MediaSessionConnection( fun pause() { sendCommand("pause", null) } + fun next() { + sendCommand("next", null) + } - fun favorite(shouldFavorite:Boolean) { + fun favorite(shouldFavorite: Boolean) { val bundle = Bundle() bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, shouldFavorite) sendCommand(PlayerPlugin.FAVORITE, bundle) @@ -93,7 +118,15 @@ class MediaSessionConnection( sendCommand("release", null) } - fun sendNotification(name: String, author: String, url: String, coverUrl: String, isPlaying: Boolean?, bigCoverUrl: String?, isFavorite: Boolean?) { + fun sendNotification( + name: String, + author: String, + url: String, + coverUrl: String, + isPlaying: Boolean?, + bigCoverUrl: String?, + isFavorite: Boolean? + ) { val bundle = Bundle() bundle.putString("name", name) bundle.putString("author", author) @@ -110,7 +143,11 @@ class MediaSessionConnection( sendCommand("remove_notification", null) } - private fun sendCommand(command: String, bundle: Bundle? = null, callbackHandler: ResultReceiver? = null) { + private fun sendCommand( + command: String, + bundle: Bundle? = null, + callbackHandler: ResultReceiver? = null + ) { ensureMediaBrowser { ensureMediaController { it.sendCommand(command, bundle, callbackHandler) @@ -123,8 +160,10 @@ class MediaSessionConnection( if (mediaBrowser == null) { val context = weakContext.get() val serviceComponent = weakServiceComponent.get() - mediaBrowser = MediaBrowserCompat(context, serviceComponent, - mediaBrowserConnectionCallback, null) + mediaBrowser = MediaBrowserCompat( + context, serviceComponent, + mediaBrowserConnectionCallback, null + ) } mediaBrowser?.let { @@ -137,7 +176,10 @@ class MediaSessionConnection( } } catch (e: Exception) { if (e.message?.contains("connect() called while neither disconnecting nor disconnected") == true) - Log.i("Player", "MediaBrowser is CONNECT_STATE_CONNECTING(2) or CONNECT_STATE_CONNECTED(3) or CONNECT_STATE_SUSPENDED(4)") + Log.i( + "Player", + "MediaBrowser is CONNECT_STATE_CONNECTING(2) or CONNECT_STATE_CONNECTED(3) or CONNECT_STATE_SUSPENDED(4)" + ) else Log.e("Player", "Failed", e) } @@ -147,8 +189,8 @@ class MediaSessionConnection( mediaController?.let(callable) } - private inner class MediaBrowserConnectionCallback(private val context: Context) - : MediaBrowserCompat.ConnectionCallback() { + private inner class MediaBrowserConnectionCallback(private val context: Context) : + MediaBrowserCompat.ConnectionCallback() { override fun onConnected() { Log.i(TAG, "MediaBrowserConnectionCallback.onConnected : STARTED") mediaBrowser?.let { mediaBrowser -> @@ -162,7 +204,7 @@ class MediaSessionConnection( if (lastState != state.state) { Log.i(TAG, "onPlaybackStateChanged2: $state") lastState = state.state - playerChangeNotifier.notifyStateChange(state.state) +// playerChangeNotifier.notifyStateChange(state.state) } } @@ -176,16 +218,23 @@ class MediaSessionConnection( this@MediaSessionConnection.duration = duration playerChangeNotifier.notifyPositionChange(position, duration) } + "error" -> { val error = extras.getString("error") - playerChangeNotifier.notifyStateChange(PlaybackStateCompat.STATE_ERROR, error) + playerChangeNotifier.notifyStateChange( + PlaybackStateCompat.STATE_ERROR, + error + ) } + "seek-end" -> { playerChangeNotifier.notifySeekEnd() } + "next" -> { playerChangeNotifier.notifyNext() } + "previous" -> { playerChangeNotifier.notifyPrevious() } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt index 338c42ae..aab0a634 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt @@ -37,6 +37,20 @@ class MethodChannelManager(private val channel: MethodChannel) { invokeMethod("commandCenter.onPrevious", args) } + fun sendCurrentQueue( + queue: List, +// currentMediaIndex: Int, + playerId: String, + ) { + val args = MethodChannelManagerArgsBuilder() + .event("CURRENT_QUEUE") + .playerId(playerId) + .queue(queue) +// .currentMediaIndex(currentMediaIndex) + .build() + invokeMethod("GET_INFO", args) + } + private fun invokeMethod(method: String, args: Map) { channel.invokeMethod(method, args) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt new file mode 100644 index 00000000..2866469e --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt @@ -0,0 +1,24 @@ +package br.com.suamusica.player + +import com.google.gson.Gson + +class MethodChannelManagerArgsBuilder { + + private val args = mutableMapOf() + + fun build() = args + + fun event(event: String): MethodChannelManagerArgsBuilder { + args["EVENT_ARGS"] = event + return this + } + fun playerId(id: String): MethodChannelManagerArgsBuilder { + args["playerId"] = id + return this + } + + fun queue(queue: List): MethodChannelManagerArgsBuilder { + args["QUEUE_ARGS"] = Gson().toJson(queue) + return this + } +} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index d133fbe0..52c976bc 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -31,6 +31,10 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { Log.i("Player", "Notifying Player Previous") channelManager.notifyPrevious("sua-musica-player") } + fun sendCurrentQueue(queue:List) { + Log.i("Player", "Notifying Player Previous") + channelManager.sendCurrentQueue(queue,"sua-musica-player") + } fun notifyPositionChange(position: Long, duration: Long) { Log.i("Player", "Notifying Player Position change: position: $position duration: $duration") channelManager.notifyPositionChange("sua-musica-player", position, duration) 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 7590e759..6050b1cf 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt @@ -1,6 +1,8 @@ package br.com.suamusica.player import android.util.Log +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -9,7 +11,7 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { +class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { companion object { // Argument names @@ -29,8 +31,10 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { // Method names const val LOAD_METHOD = "load" const val PLAY_METHOD = "play" + const val PLAY_FROM_QUEUE_METHOD = "playFromQueue" const val RESUME_METHOD = "resume" const val PAUSE_METHOD = "pause" + const val NEXT_METHOD = "next" const val STOP_METHOD = "stop" const val RELEASE_METHOD = "release" const val SEEK_METHOD = "seek" @@ -75,13 +79,16 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { override fun onDetachedFromActivityForConfigChanges() { Log.d(TAG, "onDetachedFromActivityForConfigChanges") } + override fun onReattachedToActivityForConfigChanges(p0: ActivityPluginBinding) { Log.d(TAG, "onReattachedToActivityForConfigChanges") } + override fun onDetachedFromActivity() { Log.d(TAG, "onDetachedFromActivity") alreadyAttachedToActivity = false } + override fun onMethodCall(call: MethodCall, response: MethodChannel.Result) { try { handleMethodCall(call, response) @@ -92,26 +99,46 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { } private fun handleMethodCall(call: MethodCall, response: MethodChannel.Result) { - val cookie = call.argument("cookie") - PlayerSingleton.externalPlayback = call.argument("externalplayback") - Log.d(TAG, "method: ${call.method} cookie: $cookie externalPlayback: ${PlayerSingleton.externalPlayback}") - Log.d(TAG, "TESTE1 method: ${call.method} | ${call.argument(IS_FAVORITE_ARGUMENT)}") + val cookie: String? + if (call.method == "enqueue") { + val listMedia: List> = call.arguments()!! + cookie = listMedia[0]["cookie"]!! + PlayerSingleton.externalPlayback = listMedia[0]["externalplayback"]!! == "true" + } else { + cookie = call.argument("cookie") + PlayerSingleton.externalPlayback = call.argument("externalplayback") + } + Log.d( + TAG, + "method: ${call.method}" + ) when (call.method) { LOAD_METHOD -> { - val name = call.argument(NAME_ARGUMENT)!! - val author = call.argument(AUTHOR_ARGUMENT)!! - val url = call.argument(URL_ARGUMENT)!! - val coverUrl = call.argument(COVER_URL_ARGUMENT)!! - val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) - val position = call.argument(POSITION_ARGUMENT) - val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) - - PlayerSingleton.mediaSessionConnection?.prepare(cookie!!, Media(name, author, url, coverUrl,bigCoverUrl, isFavorite)) - position?.let { - PlayerSingleton.mediaSessionConnection?.seek(it.toLong(), false) - } - PlayerSingleton.mediaSessionConnection?.sendNotification(name, author, url, coverUrl, null, bigCoverUrl,isFavorite) - Log.d(TAG, "method: ${call.method} name: $name author: $author") +// val name = call.argument(NAME_ARGUMENT)!! +// val author = call.argument(AUTHOR_ARGUMENT)!! +// val url = call.argument(URL_ARGUMENT)!! +// val coverUrl = call.argument(COVER_URL_ARGUMENT)!! +// val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) +// val position = call.argument(POSITION_ARGUMENT) +// val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) +// +// PlayerSingleton.mediaSessionConnection?.prepare( +// cookie!!, +// Media(name, author, url, coverUrl, bigCoverUrl, isFavorite) +// ) +// position?.let { +// PlayerSingleton.mediaSessionConnection?.seek(it.toLong(), false) +// } +// PlayerSingleton.mediaSessionConnection?.sendNotification( +// name, +// author, +// url, +// coverUrl, +// null, +// bigCoverUrl, +// isFavorite +// ) +// Log.d(TAG, "method: ${call.method} name: $name author: $author") } SEND_NOTIFICATION -> { val name = call.argument(NAME_ARGUMENT)!! @@ -122,74 +149,121 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin,ActivityAware { val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) - PlayerSingleton.mediaSessionConnection?.sendNotification(name, author, url, coverUrl, isPlaying, bigCoverUrl, isFavorite) + PlayerSingleton.mediaSessionConnection?.sendNotification( + name, + author, + url, + coverUrl, + isPlaying, + bigCoverUrl, + isFavorite + ) + } + + "enqueue" -> { + val listMedia: List> = call.arguments()!! + val arg = listMedia[0] + val list = listMedia.drop(1) + val json = Gson().toJson(list) + PlayerSingleton.mediaSessionConnection?.enqueue(arg["cookie"] as String, json, arg["autoPlay"] as Boolean) } PLAY_METHOD -> { - val name = call.argument(NAME_ARGUMENT)!! - val author = call.argument(AUTHOR_ARGUMENT)!! - val url = call.argument(URL_ARGUMENT)!! - val coverUrl = call.argument(COVER_URL_ARGUMENT)!! - val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) - val position = call.argument(POSITION_ARGUMENT) - val loadOnly = call.argument(LOAD_ONLY)!! - val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) + PlayerSingleton.mediaSessionConnection?.play() + } - PlayerSingleton.mediaSessionConnection?.prepare(cookie!!, Media(name, author, url, coverUrl, bigCoverUrl, isFavorite)) - Log.d(TAG, "before prepare: cookie: $cookie") - position?.let { - PlayerSingleton.mediaSessionConnection?.seek(it.toLong(), true) - } + NEXT_METHOD ->{ + PlayerSingleton.mediaSessionConnection?.next() + } - if (!loadOnly) { - PlayerSingleton.mediaSessionConnection?.play() - } + PLAY_FROM_QUEUE_METHOD -> { +// val name = call.argument(NAME_ARGUMENT)!! +// val author = call.argument(AUTHOR_ARGUMENT)!! +// val url = call.argument(URL_ARGUMENT)!! +// val coverUrl = call.argument(COVER_URL_ARGUMENT)!! +// val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) +// val position = call.argument(POSITION_ARGUMENT) +// val loadOnly = call.argument(LOAD_ONLY)!! +// val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) +// +// PlayerSingleton.mediaSessionConnection?.prepare( +// cookie!!, +// Media(name, author, url, coverUrl, bigCoverUrl, isFavorite) +// ) +// Log.d(TAG, "before prepare: cookie: $cookie") +// position?.let { +// PlayerSingleton.mediaSessionConnection?.seek(it.toLong(), true) +// } +// +// if (!loadOnly) { +// PlayerSingleton.mediaSessionConnection?.play() +// } + val position = call.argument(POSITION_ARGUMENT) ?: 0 + PlayerSingleton.mediaSessionConnection?.play( + position + ) } + RESUME_METHOD -> { PlayerSingleton.mediaSessionConnection?.play() } + PAUSE_METHOD -> { PlayerSingleton.mediaSessionConnection?.pause() } + "ads_playing" -> { PlayerSingleton.mediaSessionConnection?.adsPlaying() } + STOP_METHOD -> { PlayerSingleton.mediaSessionConnection?.stop() } + RELEASE_METHOD -> { PlayerSingleton.mediaSessionConnection?.release() } + SEEK_METHOD -> { val position = call.argument(POSITION_ARGUMENT)!! PlayerSingleton.mediaSessionConnection?.seek(position, true) } + REMOVE_NOTIFICATION_METHOD -> { PlayerSingleton.mediaSessionConnection?.removeNotification() } + SET_VOLUME_METHOD -> { } + GET_DURATION_METHOD -> { response.success(PlayerSingleton.mediaSessionConnection?.duration) return } + GET_CURRENT_POSITION_METHOD -> { response.success(PlayerSingleton.mediaSessionConnection?.currentPosition) return } + SET_RELEASE_MODE_METHOD -> { val releaseModeName = call.argument(RELEASE_MODE_ARGUMENT) - val releaseMode = ReleaseMode.valueOf(releaseModeName!!.substring("ReleaseMode.".length)) + val releaseMode = + ReleaseMode.valueOf(releaseModeName!!.substring("ReleaseMode.".length)) PlayerSingleton.mediaSessionConnection?.releaseMode = releaseMode.ordinal } + DISABLE_NOTIFICATION_COMMANDS -> { } + ENABLE_NOTIFICATION_COMMANDS -> { } + CAN_PLAY -> { // no operation required on Android } + else -> { response.notImplemented() return diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt index 8170ecdc..9e72ff98 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 @@ -9,12 +9,14 @@ object PlayerSingleton { var mediaSessionConnection: MediaSessionConnection? = null var externalPlayback: Boolean? = false private const val TAG = "Player" + var playerChangeNotifier: PlayerChangeNotifier? = null fun setChannel(c: MethodChannel, context: Context) { channel = c + playerChangeNotifier = PlayerChangeNotifier(MethodChannelManager(c)) mediaSessionConnection = MediaSessionConnection( context, - PlayerChangeNotifier(MethodChannelManager(c)) + playerChangeNotifier!! ) } diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 61cd43d5..bf514b27 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -127,8 +127,8 @@ class _SMPlayerState extends State { playlistId: 0, ); - player.enqueue(media1); - player.enqueue(media2); + player.enqueue(media: media1); + player.enqueue(media: media2); if (!mounted) return; diff --git a/packages/player/lib/player.dart b/packages/player/lib/player.dart index 6087db08..4aeb277f 100644 --- a/packages/player/lib/player.dart +++ b/packages/player/lib/player.dart @@ -7,4 +7,5 @@ export 'src/event.dart'; export 'src/duration_change_event.dart'; export 'src/position_change_event.dart'; export 'src/before_play_event.dart'; -export 'src/repeat_mode.dart'; \ No newline at end of file +export 'src/current_queue_updated.dart'; +export 'src/repeat_mode.dart'; diff --git a/packages/player/lib/src/current_queue_updated.dart b/packages/player/lib/src/current_queue_updated.dart new file mode 100644 index 00000000..e093a43e --- /dev/null +++ b/packages/player/lib/src/current_queue_updated.dart @@ -0,0 +1,11 @@ +import 'package:smplayer/src/event.dart'; +import 'package:smplayer/src/media.dart'; + +class CurrentQueueUpdated extends Event { + CurrentQueueUpdated({ + required super.type, + required List queue, + required super.queuePosition, + required super.media, + }); +} diff --git a/packages/player/lib/src/event.dart b/packages/player/lib/src/event.dart index ab6be747..eea68866 100644 --- a/packages/player/lib/src/event.dart +++ b/packages/player/lib/src/event.dart @@ -6,6 +6,7 @@ class Event { required this.type, required this.media, required this.queuePosition, + this.queue, this.error, this.errorType, this.position, @@ -19,6 +20,7 @@ class Event { final int queuePosition; final Duration? position; final Duration? duration; + final List? queue; @override String toString() => @@ -38,5 +40,5 @@ class Event { duration == other.duration; @override - int get hashCode => [type, media, error, errorType].hashCode; + int get hashCode => [type, media, error, errorType, queue].hashCode; } diff --git a/packages/player/lib/src/event_type.dart b/packages/player/lib/src/event_type.dart index b7e15ff7..250111eb 100644 --- a/packages/player/lib/src/event_type.dart +++ b/packages/player/lib/src/event_type.dart @@ -2,6 +2,7 @@ enum EventType { PLAY_REQUESTED, PLAY_NOTIFICATION, BEFORE_PLAY, + UPDATE_QUEUE, BUFFERING, PLAYING, PAUSE_REQUEST, diff --git a/packages/player/lib/src/media.dart b/packages/player/lib/src/media.dart index dacd1f20..ce6be301 100644 --- a/packages/player/lib/src/media.dart +++ b/packages/player/lib/src/media.dart @@ -194,6 +194,8 @@ class Media { extension ListMediaToListStringCompressed on List { List get toListStringCompressed => map((e) => e.toString()).toList(); + + List> get toListJson => map((e) => e.toJson()).toList(); } extension ListStringToListPlayable on List { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index f73f7f22..153d06b9 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'dart:io'; +import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:smaws/aws.dart'; import 'package:flutter/services.dart'; @@ -101,10 +101,14 @@ class Player { .then((result) => result ?? Future.value(Ok)); } - Future enqueue( - Media media, - ) async { - _queue.add(media); + Future enqueue({ + required Media media, + bool autoPlay = false, + }) async { + enqueueAll( + [media], + autoPlay: autoPlay, + ); return Ok; } @@ -112,12 +116,34 @@ class Player { List items, { bool shouldRemoveFirst = false, bool saveOnTop = false, + bool autoPlay = false, }) async { - _queue.addAll( - items, - shouldRemoveFirst: shouldRemoveFirst, - saveOnTop: saveOnTop, - ); + // _queue.addAll( + // items, + // shouldRemoveFirst: shouldRemoveFirst, + // saveOnTop: saveOnTop, + // ); + + //ENQUEUE NO PLAYER + // if(android) + _cookies = await cookieSigner(); + String cookie = _cookies!.toHeaders(); + _channel + .invokeMethod( + 'enqueue', + items.toListJson + ..insert( + 0, + { + 'playerId': playerId, + 'cookie': cookie, + 'shallSendEvents': _shallSendEvents, + 'externalplayback': externalPlayback, + 'autoPlay': autoPlay, + }, + ), + ) + .then((result) => result ?? Future.value(Ok)); return Ok; } @@ -133,10 +159,10 @@ class Player { return Ok; } - Future removeNotificaton() async { - await _invokeMethod('remove_notification'); - return Ok; - } + // Future removeNotificaton() async { + // await _invokeMethod('remove_notification'); + // return Ok; + // } Future adsPlaying() async { await _invokeMethod('ads_playing'); @@ -153,50 +179,50 @@ class Player { return Ok; } - Future sendNotification({ - bool? isPlaying, - bool? isFavorite, - Duration? position, - Duration? duration, - }) async { - if (_queue.size > 0) { - if (_queue.current == null) { - _queue.move(0); - } - final media = _queue.current!; - final data = { - 'albumId': media.albumId.toString(), - 'albumTitle': media.albumTitle, - 'name': media.name, - 'author': media.author, - 'url': media.url, - 'coverUrl': media.coverUrl, - 'bigCoverUrl': media.bigCoverUrl, - 'loadOnly': false, - 'isLocal': media.isLocal, - }; - - if (position != null) { - data['position'] = position.inMilliseconds; - } - - if (duration != null) { - data['duration'] = duration.inMilliseconds; - } - - if (isPlaying != null) { - data['isPlaying'] = isPlaying; - } - if (isFavorite != null) { - data['isFavorite'] = isFavorite; - } - - await _invokeMethod('send_notification', data); - return Ok; - } else { - return Ok; - } - } + // Future sendNotification({ + // bool? isPlaying, + // bool? isFavorite, + // Duration? position, + // Duration? duration, + // }) async { + // if (_queue.size > 0) { + // if (_queue.current == null) { + // _queue.move(0); + // } + // final media = _queue.current!; + // final data = { + // 'albumId': media.albumId.toString(), + // 'albumTitle': media.albumTitle, + // 'name': media.name, + // 'author': media.author, + // 'url': media.url, + // 'coverUrl': media.coverUrl, + // 'bigCoverUrl': media.bigCoverUrl, + // 'loadOnly': false, + // 'isLocal': media.isLocal, + // }; + + // if (position != null) { + // data['position'] = position.inMilliseconds; + // } + + // if (duration != null) { + // data['duration'] = duration.inMilliseconds; + // } + + // if (isPlaying != null) { + // data['isPlaying'] = isPlaying; + // } + // if (isFavorite != null) { + // data['isFavorite'] = isFavorite; + // } + + // await _invokeMethod('send_notification', data); + // return Ok; + // } else { + // return Ok; + // } + // } Future disableNotificatonCommands() async { await _invokeMethod('disable_notification_commands'); @@ -231,7 +257,9 @@ class Player { } } - List get items => _queue.items; + static List newQueue = []; + + List get items => newQueue; int get queuePosition => _queue.index; int get previousPlaylistIndex => _queue.previousIndex; PreviousPlaylistPosition? get previousPlaylistPosition => @@ -241,8 +269,8 @@ class Player { Media? get top => _queue.top; Future load(Media media) async => _doPlay( - _queue.current!, - shouldLoadOnly: true, + // _queue.current!, + // shouldLoadOnly: true, ); Future play( @@ -252,9 +280,9 @@ class Player { bool respectSilence = false, bool stayAwake = false, }) async { - _queue.play(media); + // _queue.play(media); _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); - return _doPlay(_queue.current!); + return _doPlay(); } Future playFromQueue( @@ -266,116 +294,134 @@ class Player { bool shallNotify = false, bool loadOnly = false, }) async { - Media? media = _queue.item(pos); - if (media != null) { - final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; - if (!loadOnly) { - _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); - } - return _doPlay( - _queue.move(pos)!, - shallNotify: shallNotify, - mediaUrl: mediaUrl, - shouldLoadOnly: loadOnly, - position: position, - ); - } else { - return NotOk; - } + // Media? media = _queue.item(pos); + // if (media != null) { + // final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; + if (!loadOnly) { + _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); + } + // return _doPlay( + // _queue.move(pos)!, + // shallNotify: shallNotify, + // mediaUrl: mediaUrl, + // shouldLoadOnly: loadOnly, + // position: position, + // ); + // } else { + // return NotOk; + // } + return _channel.invokeMethod( + 'playFromQueue', {'position': pos}).then((result) => result); + // _channel + // .invokeMethod( + // 'enqueue', + // items.toListJson + // ..insert( + // 0, + // { + // 'playerId': playerId, + // 'cookie': cookie, + // 'shallSendEvents': _shallSendEvents, + // 'externalplayback': externalPlayback, + // }, + // ), + // ) + // .then((result) => result ?? Future.value(Ok)); } Future _doPlay( - Media media, { - double? volume, - Duration? position, - bool? respectSilence, - bool? stayAwake, - bool? shallNotify, - bool? shouldLoadOnly, - String? mediaUrl, - }) async { - volume ??= 1.0; - respectSilence ??= false; - stayAwake ??= false; - shallNotify ??= false; - shouldLoadOnly ??= false; - - if (shallNotify) { - _notifyChangeToNext(media); - } - mediaUrl ??= (await localMediaValidator?.call(media)) ?? media.url; - //If it is local, check if it exists before playing it. - - if (!mediaUrl.startsWith("http")) { - if (!File(mediaUrl).existsSync() && media.fallbackUrl != null) { - //Should we remove from DB?? - mediaUrl = media.fallbackUrl; - } - } + // Media media, { + // double? volume, + // Duration? position, + // bool? respectSilence, + // bool? stayAwake, + // bool? shallNotify, + // bool? shouldLoadOnly, + // String? mediaUrl, + // } + ) async { + // volume ??= 1.0; + // respectSilence ??= false; + // stayAwake ??= false; + // shallNotify ??= false; + // shouldLoadOnly ??= false; + + // if (shallNotify) { + // _notifyChangeToNext(media); + // } + // mediaUrl ??= (await localMediaValidator?.call(media)) ?? media.url; + // //If it is local, check if it exists before playing it. + + // if (!mediaUrl.startsWith("http")) { + // if (!File(mediaUrl).existsSync() && media.fallbackUrl != null) { + // //Should we remove from DB?? + // mediaUrl = media.fallbackUrl; + // } + // } + + // // we need to update the value as it could have been + // // downloading and is not downloaded + // media.isLocal = !mediaUrl!.startsWith("http"); + // media.url = mediaUrl; + // if (shouldLoadOnly) { + // debugPrint("LOADING ONLY!"); + // return invokeLoad({ + // 'albumId': media.albumId.toString(), + // 'albumTitle': media.albumTitle, + // 'name': media.name, + // 'author': media.author, + // 'url': mediaUrl, + // 'coverUrl': media.coverUrl, + // 'bigCoverUrl': media.bigCoverUrl, + // 'loadOnly': true, + // 'isLocal': media.isLocal, + // 'volume': volume, + // 'position': position?.inMilliseconds, + // 'respectSilence': respectSilence, + // 'stayAwake': stayAwake, + // 'isFavorite': media.isFavorite + // }); + // } else if (autoPlay) { + // _notifyBeforePlayEvent((loadOnly) => {}); + + // return invokePlay(media, { + // 'albumId': media.albumId.toString(), + // 'albumTitle': media.albumTitle, + // 'name': media.name, + // 'author': media.author, + // 'url': mediaUrl, + // 'coverUrl': media.coverUrl, + // 'bigCoverUrl': media.bigCoverUrl, + // 'loadOnly': false, + // 'isLocal': media.isLocal, + // 'volume': volume, + // 'position': position?.inMilliseconds, + // 'respectSilence': respectSilence, + // 'stayAwake': stayAwake, + // 'isFavorite': media.isFavorite + // }); + // } else { + // _notifyBeforePlayEvent((loadOnly) { + // invokePlay(media, { + // 'albumId': media.albumId.toString(), + // 'albumTitle': media.albumTitle, + // 'name': media.name, + // 'author': media.author, + // 'url': mediaUrl, + // 'coverUrl': media.coverUrl, + // 'bigCoverUrl': media.bigCoverUrl, + // 'loadOnly': loadOnly, + // 'isLocal': media.isLocal, + // 'volume': volume, + // 'position': position?.inMilliseconds, + // 'respectSilence': respectSilence, + // 'stayAwake': stayAwake, + // 'isFavorite': media.isFavorite + // }); + // }); + _channel.invokeMethod('play'); - // we need to update the value as it could have been - // downloading and is not downloaded - media.isLocal = !mediaUrl!.startsWith("http"); - media.url = mediaUrl; - if (shouldLoadOnly) { - debugPrint("LOADING ONLY!"); - return invokeLoad({ - 'albumId': media.albumId.toString(), - 'albumTitle': media.albumTitle, - 'name': media.name, - 'author': media.author, - 'url': mediaUrl, - 'coverUrl': media.coverUrl, - 'bigCoverUrl': media.bigCoverUrl, - 'loadOnly': true, - 'isLocal': media.isLocal, - 'volume': volume, - 'position': position?.inMilliseconds, - 'respectSilence': respectSilence, - 'stayAwake': stayAwake, - 'isFavorite': media.isFavorite - }); - } else if (autoPlay) { - _notifyBeforePlayEvent((loadOnly) => {}); - - return invokePlay(media, { - 'albumId': media.albumId.toString(), - 'albumTitle': media.albumTitle, - 'name': media.name, - 'author': media.author, - 'url': mediaUrl, - 'coverUrl': media.coverUrl, - 'bigCoverUrl': media.bigCoverUrl, - 'loadOnly': false, - 'isLocal': media.isLocal, - 'volume': volume, - 'position': position?.inMilliseconds, - 'respectSilence': respectSilence, - 'stayAwake': stayAwake, - 'isFavorite': media.isFavorite - }); - } else { - _notifyBeforePlayEvent((loadOnly) { - invokePlay(media, { - 'albumId': media.albumId.toString(), - 'albumTitle': media.albumTitle, - 'name': media.name, - 'author': media.author, - 'url': mediaUrl, - 'coverUrl': media.coverUrl, - 'bigCoverUrl': media.bigCoverUrl, - 'loadOnly': loadOnly, - 'isLocal': media.isLocal, - 'volume': volume, - 'position': position?.inMilliseconds, - 'respectSilence': respectSilence, - 'stayAwake': stayAwake, - 'isFavorite': media.isFavorite - }); - }); - - return Ok; - } + return Ok; } Future invokePlay(Media media, Map args) async { @@ -433,9 +479,9 @@ class Player { } else { _notifyChangeToPrevious(previous); return _doPlay( - previous, - mediaUrl: mediaUrl, - ); + // previous, + // mediaUrl: mediaUrl, + ); } } @@ -459,62 +505,14 @@ class Player { bool? shallNotify, String? mediaUrl, }) async { - final current = _queue.current; - Media? next; - - // first case, nothing has yet played - // therefore, we need to play the first - // track on the key and treat this as a - // play method invocation - if (current == null) { - next = _queue.next(); - if (next == null) { - // nothing to play - return NotOk; - } - // notice that in this case - // we do not emit the NEXT event - // we only play the track - return _doPlay( - next, - shallNotify: shallNotify, - ); - } - - next = _queue.next(); - if (next == null) { - if ((state == PlayerState.PLAYING || state == PlayerState.PAUSED) && - repeatMode == RepeatMode.NONE) { - return _forward(current); - } else { - if (repeatMode == RepeatMode.NONE) { - _queue.setIndex = 0; - return NotOk; - } else if (repeatMode == RepeatMode.QUEUE) { - next = _queue.restart(); - } else if (repeatMode == RepeatMode.TRACK) { - repeatMode = RepeatMode.QUEUE; - next = _queue.restart(); - } else { - // this should not happen! - return NotOk; - } - } - } - if (repeatMode == RepeatMode.TRACK) { - repeatMode = RepeatMode.QUEUE; - } - return _doPlay( - next, - shallNotify: shallNotify, - mediaUrl: mediaUrl, - ); + return _channel.invokeMethod('next').then((result) => result); } Future pause() async { _notifyPlayerStatusChangeEvent(EventType.PAUSE_REQUEST); - return await _invokeMethod('pause'); + // return await _invokeMethod('pause'); + return _channel.invokeMethod('pause').then((result) => result); } void addUsingPlayer(Event event) => _addUsingPlayer(player, event); @@ -792,6 +790,32 @@ class Player { "", ); + break; + case 'GET_INFO': + final queue = callArgs['QUEUE_ARGS']; + final parsed = json.decode(queue) as List; + final a = parsed.map((json) => Media.fromJson(json)).toList(); + _addUsingPlayer( + player, + Event( + type: EventType.UPDATE_QUEUE, + queue: a, + queuePosition: 0, + media: Media( + id: 0, + albumId: 0, + albumTitle: "0", + name: "0", + ownerId: 0, + author: "0", + url: "0", + isLocal: false, + coverUrl: "0", + bigCoverUrl: "0", + ), + ), + ); + newQueue.addAll(a); break; default: _log('Unknown method ${call.method} '); diff --git a/packages/player/test/player_test.dart b/packages/player/test/player_test.dart index 25a526b4..3ab769a1 100644 --- a/packages/player/test/player_test.dart +++ b/packages/player/test/player_test.dart @@ -71,23 +71,23 @@ void main() { test('Adding media to an empty queue shall make it the queue top', () async { final subject = createPlayer(); - subject.enqueue(media1); + subject.enqueue(media: media1); expect(subject.size, 1); expect(subject.top, media1); }); test('The queue shall support multiple items', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); expect(subject.size, 3); expect(subject.top, media1); expect(subject.items, [media1, media2, media3]); }); test('Playing a media shall replace the queue top', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); + subject.enqueue(media: media1); + subject.enqueue(media: media2); subject.play(media3); expect(subject.size, 2); expect(subject.top, media3); @@ -96,9 +96,9 @@ void main() { test('Removing a media shall be supported', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); subject.removeByPosition(positionsToDelete: [1], isShuffle: false); @@ -159,18 +159,18 @@ void main() { test('Rewind on a queue that was not played shall raise an error', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); expect(await subject.rewind(), 1); }); test( 'Rewind shall be supported', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); subject.play(media1); subject.rewind(); @@ -190,17 +190,17 @@ void main() { test('Previous on a queue that was not played shall raise an error', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); final a = await subject.previous(); expect(a, 1); }); test('Previous shall act as rewind', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); subject.play(media1); subject.previous(); @@ -213,9 +213,9 @@ void main() { 'Two consecutive previous invocation shall really go the previous track', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); expect(await subject.next(), Player.Ok); expect(subject.size, 3); @@ -238,9 +238,9 @@ void main() { () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); expect(await subject.next(), Player.Ok); expect(subject.size, 3); @@ -266,9 +266,9 @@ void main() { test('Next on a queue that was not played shall start playing it', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); expect(await subject.next(), Player.Ok); expect(subject.size, 3); @@ -278,9 +278,9 @@ void main() { test('Next on a queue that is playing shall move to the next', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); expect(await subject.next(), Player.Ok); @@ -296,9 +296,9 @@ void main() { test('Next when reaching the end of the queue shall return null', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); expect(await subject.next(), Player.Ok); expect(subject.size, 3); @@ -342,17 +342,17 @@ void main() { test('Top on an unplayed queue shall return the top of the queue', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); expect(subject.size, 3); expect(subject.top, media1); }); test('Current on an unplayed queue shall return media1', () async { final subject = createPlayer(); - subject.enqueue(media1); - subject.enqueue(media2); - subject.enqueue(media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); expect(subject.size, 3); expect(subject.current, media1); }); From fac7d75c4b4a42774df7db126aa93c76859fccf9 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 30 Aug 2024 16:46:56 -0300 Subject: [PATCH 17/70] wip --- packages/player/android/build.gradle | 2 + .../kotlin/br/com/suamusica/player/Media.kt | 21 ++++++-- .../player/MediaButtonEventHandler.kt | 48 +++++++++++-------- .../br/com/suamusica/player/MediaService.kt | 1 + .../br/com/suamusica/player/PlayerPlugin.kt | 3 +- 5 files changed, 49 insertions(+), 26 deletions(-) diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index cbc1b14b..c3d0a5f0 100644 --- a/packages/player/android/build.gradle +++ b/packages/player/android/build.gradle @@ -60,6 +60,8 @@ dependencies { implementation "androidx.media3:media3-ui:$media3_version" // implementation files('/Users/suamusica/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar') + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' + implementation "com.google.code.gson:gson:2.10.1" } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt index 99b499d9..f66c706b 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt @@ -1,11 +1,26 @@ package br.com.suamusica.player class Media( + val id: Int, val name: String, + val ownerId: Int?, + val albumId: Int?, + val albumTitle: String?, val author: String, val url: String, + val isLocal: Boolean?, + val localPath : String?, val coverUrl: String, - val bigCoverUrl: String?, - // val albumId:int? - val isFavorite: Boolean? + val bigCoverUrl: String, + val isVerified: Boolean?, + val shareUrl: String?, + val playlistId: Int?, + val isSpot: Boolean?, + val isFavorite: Boolean?, + val fallbackUrl: String?, + val indexInPlaylist: Int?, + val categoryId: Int?, + val playlistTitle: String?, + val playlistCoverUrl:String?, + val playlistOwnerId: Int?, ) \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt index 329164c2..3fd3c643 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 @@ -25,6 +25,8 @@ import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json @UnstableApi class MediaButtonEventHandler( @@ -125,30 +127,34 @@ class MediaButtonEventHandler( mediaService.removeNotification() } if (customCommand.customAction == "enqueue") { - val listType = object : TypeToken?>() {}.type + val listType = object : TypeToken>() {}.type + val gson = Gson() + val json = args.getString("json") val yourClassList: List = - Gson().fromJson(args.getString("json"), listType) + gson.fromJson(json, listType) + val userList: List = Json.decodeFromString(json!!) + mediaService.load(args.getString("cookie")!!,yourClassList,args.getBoolean("autoPlay")) } - 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) - } - } +// 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) +// } +// } return Futures.immediateFuture( SessionResult(SessionResult.RESULT_SUCCESS) ) 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 eecb956a..8072f914 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 @@ -297,6 +297,7 @@ class MediaService : MediaSessionService() { setArtworkData(stream.toByteArray(), PICTURE_TYPE_FRONT_COVER) setArtist(media.author) setTitle(media.name) +// setArtworkUri(Uri.parse(media.bigCoverUrl)) setDisplayTitle(media.name) } val metadata = metadataBuilder.build() 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 6050b1cf..a26ba6f2 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 @@ -163,8 +163,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { "enqueue" -> { val listMedia: List> = call.arguments()!! val arg = listMedia[0] - val list = listMedia.drop(1) - val json = Gson().toJson(list) + val json = Gson().toJson(listMedia.drop(1)) PlayerSingleton.mediaSessionConnection?.enqueue(arg["cookie"] as String, json, arg["autoPlay"] as Boolean) } PLAY_METHOD -> { From 0ae65c4ddd62912b3d88ba6ab3f57f91a40e6e36 Mon Sep 17 00:00:00 2001 From: Alan Trope Date: Fri, 30 Aug 2024 17:09:21 -0300 Subject: [PATCH 18/70] chore: cursor find my bug --- .../kotlin/br/com/suamusica/player/Media.kt | 47 ++++++++++--------- .../player/MediaButtonEventHandler.kt | 18 ++++--- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt index f66c706b..f084caf0 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt @@ -1,26 +1,27 @@ package br.com.suamusica.player -class Media( - val id: Int, - val name: String, - val ownerId: Int?, - val albumId: Int?, - val albumTitle: String?, - val author: String, - val url: String, - val isLocal: Boolean?, - val localPath : String?, - val coverUrl: String, - val bigCoverUrl: String, - val isVerified: Boolean?, - val shareUrl: String?, - val playlistId: Int?, - val isSpot: Boolean?, - val isFavorite: Boolean?, - val fallbackUrl: String?, - val indexInPlaylist: Int?, - val categoryId: Int?, - val playlistTitle: String?, - val playlistCoverUrl:String?, - val playlistOwnerId: Int?, +import com.google.gson.annotations.SerializedName + +data class Media( + @SerializedName("id") val id: Int, + @SerializedName("name") val name: String, + @SerializedName("ownerId") val ownerId: Int, + @SerializedName("albumId") val albumId: Int, + @SerializedName("albumTitle") val albumTitle: String, + @SerializedName("author") val author: String, + @SerializedName("url") val url: String, + @SerializedName("is_local") val isLocal: Boolean, + @SerializedName("cover_url") val coverUrl: String, + @SerializedName("bigCover") val bigCoverUrl: String, + @SerializedName("is_verified") val isVerified: Boolean, + @SerializedName("shared_url") val shareUrl: String, + @SerializedName("playlist_id") val playlistId: Int, + @SerializedName("is_spot") val isSpot: Boolean, + @SerializedName("isFavorite") val isFavorite: Boolean?, + @SerializedName("fallbackUrl") val fallbackUrl: String, + @SerializedName("indexInPlaylist") val indexInPlaylist: Int?, + @SerializedName("catid") val categoryId: Int, + @SerializedName("playlistTitle") val playlistTitle: String, + @SerializedName("playlistCoverUrl") val playlistCoverUrl: String, + @SerializedName("playlistOwnerId") val playlistOwnerId: Int ) \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt index 3fd3c643..5803838e 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 @@ -25,6 +25,7 @@ import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import com.google.gson.GsonBuilder import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json @@ -127,14 +128,19 @@ class MediaButtonEventHandler( mediaService.removeNotification() } if (customCommand.customAction == "enqueue") { - val listType = object : TypeToken>() {}.type - val gson = Gson() val json = args.getString("json") - val yourClassList: List = - gson.fromJson(json, listType) - val userList: List = Json.decodeFromString(json!!) + // Log the received JSON + Log.d("Player", "First media Received JSON for enqueue: $json") + val gson = GsonBuilder().create() + val mediaListType = object : TypeToken>() {}.type + val mediaList: List = gson.fromJson(json, mediaListType) - mediaService.load(args.getString("cookie")!!,yourClassList,args.getBoolean("autoPlay")) + // Log the first item for debugging + if (mediaList.isNotEmpty()) { + Log.d("Player", "First media item: ${gson.toJson(mediaList.first())}") + } + + mediaService.load(args.getString("cookie")!!, mediaList, args.getBoolean("autoPlay")) } // if (customCommand.customAction == "prepare") { // args.let { From cb34a6d88da3c8b1d393305d0d6da980e5129f56 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 2 Sep 2024 15:14:46 -0300 Subject: [PATCH 19/70] supersonic enqueue --- .../player/MediaButtonEventHandler.kt | 30 +-- .../br/com/suamusica/player/MediaService.kt | 225 +++++----------- .../player/MediaSessionConnection.kt | 4 +- .../suamusica/player/MethodChannelManager.kt | 34 ++- .../player/MethodChannelManagerArgsBuilder.kt | 5 + .../suamusica/player/PlayerChangeNotifier.kt | 4 + .../br/com/suamusica/player/PlayerPlugin.kt | 2 +- packages/player/lib/src/event_type.dart | 1 + packages/player/lib/src/player.dart | 249 ++++++++++++------ packages/player/lib/src/queue.dart | 62 ++--- 10 files changed, 298 insertions(+), 318 deletions(-) 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 5803838e..779c6f9b 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 @@ -42,9 +42,9 @@ class MediaButtonEventHandler( Log.d("Player", "onConnect") val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().apply { - add(SessionCommand("notification_next", Bundle.EMPTY)) + add(SessionCommand("next", Bundle.EMPTY)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - add(SessionCommand("notification_previous", Bundle.EMPTY)) + add(SessionCommand("previous", Bundle.EMPTY)) } add(SessionCommand("notification_favoritar", Bundle.EMPTY)) add(SessionCommand("notification_desfavoritar", Bundle.EMPTY)) @@ -114,11 +114,11 @@ class MediaButtonEventHandler( if (customCommand.customAction == "playFromQueue") { mediaService.playFromQueue(args.getInt(POSITION_ARGUMENT)) } - if (customCommand.customAction == "notification_previous") { - PlayerSingleton.previous() + if (customCommand.customAction == "previous") { + session.player.seekToPrevious() } - if (customCommand.customAction == "notification_next") { - PlayerSingleton.next() + if (customCommand.customAction == "next") { + session.player.seekToNext() } if (customCommand.customAction == "pause") { mediaService.pause() @@ -140,7 +140,7 @@ class MediaButtonEventHandler( Log.d("Player", "First media item: ${gson.toJson(mediaList.first())}") } - mediaService.load(args.getString("cookie")!!, mediaList, args.getBoolean("autoPlay")) + mediaService.enqueue(args.getString("cookie")!!, mediaList, args.getBoolean("autoPlay"),args.getInt("startFromPos")) } // if (customCommand.customAction == "prepare") { // args.let { @@ -180,9 +180,9 @@ class MediaButtonEventHandler( .setEnabled(true) .build(), CommandButton.Builder() - .setDisplayName("notification_next") + .setDisplayName("next") .setIconResId(drawable.media3_icon_next) - .setSessionCommand(SessionCommand("notification_next", Bundle.EMPTY)) + .setSessionCommand(SessionCommand("next", Bundle.EMPTY)) .setEnabled(true) .build(), ) @@ -190,9 +190,9 @@ class MediaButtonEventHandler( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { list.plus( CommandButton.Builder() - .setDisplayName("notification_previous") + .setDisplayName("previous") .setIconResId(drawable.media3_icon_previous) - .setSessionCommand(SessionCommand("notification_previous", Bundle.EMPTY)) + .setSessionCommand(SessionCommand("previous", Bundle.EMPTY)) .build() ) } else { @@ -208,12 +208,12 @@ class MediaButtonEventHandler( controllerInfo: MediaSession.ControllerInfo, intent: Intent ): Boolean { - onMediaButtonEventHandler(intent) + onMediaButtonEventHandler(intent, session) return true } @UnstableApi - fun onMediaButtonEventHandler(intent: Intent?) { + fun onMediaButtonEventHandler(intent: Intent?,session: MediaSession,) { if (intent == null) { return @@ -252,12 +252,12 @@ class MediaButtonEventHandler( KeyEvent.KEYCODE_MEDIA_NEXT -> { Log.d("Player", "Player: Key Code : Next") - PlayerSingleton.next() + session.player.seekToNext() } KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { Log.d("Player", "Player: Key Code : Previous") - PlayerSingleton.previous() + session.player.seekToPrevious() } KeyEvent.KEYCODE_MEDIA_STOP -> { 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 8072f914..c94c1f4e 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 @@ -14,7 +14,6 @@ import android.os.Bundle import android.os.Handler import android.os.PowerManager import android.support.v4.media.session.PlaybackStateCompat -import android.util.Log import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem @@ -26,6 +25,7 @@ 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.Log import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util import androidx.media3.datasource.DataSource @@ -49,6 +49,10 @@ import androidx.media3.session.SessionResult import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.ListenableFuture +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import java.io.File import java.util.concurrent.TimeUnit @@ -83,8 +87,9 @@ class MediaService : MediaSessionService() { private lateinit var mediaButtonEventHandler: MediaButtonEventHandler private var customMedia3Notification: MediaNotification? = null + private val artCache = HashMap() + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d(TAG, "onStartCommand") super.onStartCommand(intent, flags, startId) return Service.START_STICKY } @@ -119,7 +124,6 @@ class MediaService : MediaSessionService() { dataSourceBitmapLoader = DataSourceBitmapLoader(applicationContext) - Log.d(TAG, "MEDIA3 - handleCustomCommand ${Build.VERSION_CODES.TIRAMISU}") player?.let { mediaSession = MediaSession.Builder(this, it) .setBitmapLoader(CacheBitmapLoader(dataSourceBitmapLoader)) @@ -134,15 +138,9 @@ class MediaService : MediaSessionService() { actionFactory: MediaNotification.ActionFactory, onNotificationChangedCallback: MediaNotification.Provider.Callback ): MediaNotification { - Log.d(TAG, "#MEDIA3# - createNotification | ${mediaSession.id}") val defaultMediaNotificationProvider = DefaultMediaNotificationProvider( applicationContext, -// DefaultMediaNotificationProvider( -// applicationContext, -// { R.string.notification_id }, -// NOW_PLAYING_CHANNEL, -// R.string.notification_channel ).apply { setSmallIcon(R.drawable.ic_notification) } @@ -166,7 +164,6 @@ class MediaService : MediaSessionService() { action: String, extras: Bundle ): Boolean { - Log.d(TAG, "#MEDIA3# - handleCustomCommand $action") return false } }) @@ -182,10 +179,7 @@ class MediaService : MediaSessionService() { val shouldStopService = !player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == Player.STATE_ENDED - Log.d(TAG, "#MEDIA3# - onTaskRemoved $shouldStopService") if (shouldStopService) { - // Stop the service if not playing, continue playing in the background - // otherwise. stopSelf() } isServiceRunning() @@ -219,21 +213,32 @@ class MediaService : MediaSessionService() { return false } - fun load(cookie: String, medias: List, autoPlay: Boolean) { - val sources = listOf() -// player?.clearMediaItems() - medias.forEach { - val a = prepare(cookie, it) - player?.addMediaSource(a) - } - player?.prepare() - PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias) - if (autoPlay) { - play() + fun enqueue(cookie: String, medias: List, autoPlay: Boolean, startFromPos: Int) { + if (medias.isNotEmpty()) { + // Prepare the first media source outside the coroutine + val firstMediaSource = prepare(cookie, medias[0]) + player?.setMediaSource(firstMediaSource) + player?.prepare() + if (autoPlay) { + play() + } + // Use coroutine to prepare and add the remaining media sources + CoroutineScope(Dispatchers.Main).launch { + for (i in 1 until medias.size) { + val mediaSource = withContext(Dispatchers.IO) { + Log.i(TAG, "CoroutineScope: enqueue: ${medias[i].name}") + prepare(cookie, medias[i]) + } + player?.addMediaSource(mediaSource) + } + player?.prepare() + + } + PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias) } } - fun prepare(cookie: String, media: Media): MediaSource { + private fun prepare(cookie: String, media: Media): MediaSource { this.media = media val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setReadTimeoutMs(15 * 1000) @@ -243,19 +248,20 @@ class MediaService : MediaSessionService() { dataSourceFactory.setDefaultRequestProperties(mapOf("Cookie" to cookie)) 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.CONTENT_TYPE_HLS}") + return when (type) { - C.CONTENT_TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory) - .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) - .setAllowChunklessPreparation(true).createMediaSource(mediaItem) + C.CONTENT_TYPE_HLS -> { + HlsMediaSource.Factory(dataSourceFactory) + .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) + .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) { dataSourceFactory @@ -283,9 +289,11 @@ class MediaService : MediaSessionService() { } val stream = ByteArrayOutputStream() - val art = try { - dataSourceBitmapLoader.loadBitmap(Uri.parse(media.bigCoverUrl!!)) - .get(5000, TimeUnit.MILLISECONDS) + val art = artCache[media.bigCoverUrl] ?: try { + dataSourceBitmapLoader.loadBitmap(Uri.parse(media.bigCoverUrl)) + .get(5000, TimeUnit.MILLISECONDS).also { + artCache[media.bigCoverUrl] = it + } } catch (e: Exception) { BitmapFactory.decodeResource(resources, R.drawable.default_art) } @@ -297,7 +305,6 @@ class MediaService : MediaSessionService() { setArtworkData(stream.toByteArray(), PICTURE_TYPE_FRONT_COVER) setArtist(media.author) setTitle(media.name) -// setArtworkUri(Uri.parse(media.bigCoverUrl)) setDisplayTitle(media.name) } val metadata = metadataBuilder.build() @@ -305,29 +312,17 @@ class MediaService : MediaSessionService() { } fun play() { - player?.play() + performAndEnableTracking { + player?.play() + } } fun playFromQueue(position: Int) { player?.seekTo(position, 0) + PlayerSingleton.playerChangeNotifier?.currentMediaIndex(player?.currentMediaItemIndex ?: 0) } - // fun adsPlaying() { -// val oldItem = player!!.currentMediaItem!! -// val newItem = oldItem -// .buildUpon().setMediaMetadata( -// oldItem.mediaMetadata.buildUpon() -// .setTitle("Propaganda") -// .setDescription("Propaganda") -// .build() -// ) -// .build() -// player!!.replaceMediaItem(0, newItem) -// player!!.prepare() -// } fun removeNotification() { -// player?.stop() -// shouldStartService() } fun seek(position: Long, playWhenReady: Boolean) { @@ -341,20 +336,6 @@ class MediaService : MediaSessionService() { } } -// fun returnCurrentQueue() { -// try { -// player?.let { -// PlayerSingleton.playerChangeNotifier?.sendCurrentQueue( -// currentQueue(), -// it.currentMediaItemIndex -// ) -// } -// } catch (e: java.lang.Exception) { -// OnePlayerSingleton.log(TAG, "returnCurrentQueue Exception - $e") -// throw e -// } -// } - fun stop() { performAndDisableTracking { player?.stop() @@ -386,7 +367,7 @@ class MediaService : MediaSessionService() { extra.putString("type", "position") extra.putLong("position", position) extra.putLong("duration", duration) - mediaSession?.setSessionExtras(extra) + mediaSession.setSessionExtras(extra) } } @@ -423,54 +404,35 @@ class MediaService : MediaSessionService() { private fun playerEventListener(): Player.Listener { return object : Player.Listener { - override fun onTimelineChanged(timeline: Timeline, reason: Int) { - Log.i(TAG, "onTimelineChanged: timeline: $timeline reason: $reason") - } - - override fun onTracksChanged(tracks: Tracks) { - Log.i(TAG, "onTracksChanged: ") - } - 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) + mediaSession.setSessionExtras(bundle) } } override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) -// if (isPlaying) { -// shouldStartService() -// } -// shouldStartService() -// val duration = player?.duration ?: 0L -// acquireLock( -// if (duration > 1L) duration + TimeUnit.MINUTES.toMillis(2) else TimeUnit.MINUTES.toMillis( -// 3 -// ) -// ) -// } else { -// stopService() -// releaseLock() -// } + PlayerSingleton.playerChangeNotifier?.notifyStateChange(if(isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED) + } + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + super.onMediaItemTransition(mediaItem, reason) + PlayerSingleton.playerChangeNotifier?.currentMediaIndex( + player?.currentMediaItemIndex ?: 0 + ) } - // override fun onPlaybackStateChanged(playbackState: Int) { super.onPlaybackStateChanged(playbackState) + PlayerSingleton.playerChangeNotifier?.notifyStateChange(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 { stopTrackingProgressAndPerformTask {} @@ -480,81 +442,14 @@ class MediaService : MediaSessionService() { } previousState = playbackState } -// override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { -// isServiceRunning() -// Log.i( -// TAG, -// "onPlayerStateChanged: playWhenReady: $playWhenReady playbackState: $playbackState currentPlaybackState: ${player?.playbackState} ServiceRunning: ${isServiceRunning()}" -// ) -// if (playWhenReady) { -// val duration = player?.duration ?: 0L -//// acquireLock( -//// if (duration > 1L) duration + TimeUnit.MINUTES.toMillis(2) else TimeUnit.MINUTES.toMillis( -//// 3 -//// ) -//// ) -// } else { -//// releaseLock() -// } -// -// if (playWhenReady && playbackState == ExoPlayer.STATE_READY) { -// // -// } else { -// if (player?.playerError != null) { -// // -// } else { -// when (playbackState) { -// ExoPlayer.STATE_IDLE -> { // 1 -// // -// } -// -// -// ExoPlayer.STATE_BUFFERING -> { // 2 -// // -// } -// -// ExoPlayer.STATE_READY -> { // 3 -// val status = -// if (playWhenReady) PlayerState.PLAYING else PlayerState.PAUSED -// if (previousState == -1) { -// // when we define that the track shall not "playWhenReady" -// // no position info is sent -// // therefore, we need to "emulate" the first position notification -// // by sending it directly -// notifyPositionChange() -// } else { -// if (status == PlayerState.PAUSED) { -// stopTrackingProgressAndPerformTask { -// // -// } -// } else { -// // -// } -// -// } -// } -// -// ExoPlayer.STATE_ENDED -> { // 4 -// stopTrackingProgressAndPerformTask { -// // -// } -// } -// } -// } -// } -// previousState = playbackState -// } override fun onRepeatModeChanged(repeatMode: Int) { - Log.i(TAG, "onRepeatModeChanged: $repeatMode") } override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { - Log.i(TAG, "onShuffleModeEnabledChanged: $shuffleModeEnabled") } override fun onPlayerError(error: PlaybackException) { - Log.e(TAG, "onPLayerError: ${error.message}", error) val bundle = Bundle() bundle.putString("type", "error") bundle.putString( @@ -563,11 +458,10 @@ class MediaService : MediaSessionService() { .contains("Permission denied") ) "Permission denied" else error.message ) - mediaSession?.setSessionExtras(bundle) + mediaSession.setSessionExtras(bundle) } override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { - Log.i(TAG, "onPlaybackParametersChanged: $playbackParameters") } } } @@ -611,7 +505,6 @@ class MediaService : MediaSessionService() { } isForegroundService = false stopSelf() - Log.i(TAG, "Stopping Service") } } } 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 91c4d658..8931dd3b 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 @@ -68,9 +68,7 @@ class MediaSessionConnection( fun playFromQueue(index: Int) { val bundle = Bundle() - - bundle.putInt("index", index) - + bundle.putInt("position", index) sendCommand("playFromQueue", bundle) } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt index aab0a634..acc6c566 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt @@ -6,34 +6,34 @@ class MethodChannelManager(private val channel: MethodChannel) { fun notifyPositionChange(playerId: String, position: Long, duration: Long) { val args = ArgsBuilder() - .playerId(playerId) - .position(position) - .duration(duration) - .build() + .playerId(playerId) + .position(position) + .duration(duration) + .build() invokeMethod("audio.onCurrentPosition", args) } fun notifyPlayerStateChange(playerId: String, state: PlayerState, error: String? = null) { val args = ArgsBuilder() - .playerId(playerId) - .state(state) - .error(error) - .build() + .playerId(playerId) + .state(state) + .error(error) + .build() invokeMethod("state.change", args) } fun notifyNext(playerId: String) { val args = ArgsBuilder() - .playerId(playerId) - .build() + .playerId(playerId) + .build() invokeMethod("commandCenter.onNext", args) } fun notifyPrevious(playerId: String) { val args = ArgsBuilder() - .playerId(playerId) - .build() + .playerId(playerId) + .build() invokeMethod("commandCenter.onPrevious", args) } @@ -46,7 +46,6 @@ class MethodChannelManager(private val channel: MethodChannel) { .event("CURRENT_QUEUE") .playerId(playerId) .queue(queue) -// .currentMediaIndex(currentMediaIndex) .build() invokeMethod("GET_INFO", args) } @@ -56,4 +55,13 @@ class MethodChannelManager(private val channel: MethodChannel) { channel.invokeMethod(method, args) } + fun currentMediaIndex(playerId: String, currentMediaIndex: Int) { + val args = MethodChannelManagerArgsBuilder() + .event("GET_INFO") + .playerId(playerId) + .currentMediaIndex(currentMediaIndex) + .build() + invokeMethod("SET_CURRENT_MEDIA_INDEX", args) + } + } \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt index 2866469e..ec3f56ed 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt @@ -15,6 +15,11 @@ class MethodChannelManagerArgsBuilder { fun playerId(id: String): MethodChannelManagerArgsBuilder { args["playerId"] = id return this + } + + fun currentMediaIndex(index: Int): MethodChannelManagerArgsBuilder { + args["CURRENT_MEDIA_INDEX"] = index + return this } fun queue(queue: List): MethodChannelManagerArgsBuilder { diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index 52c976bc..4c753707 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -35,6 +35,10 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { Log.i("Player", "Notifying Player Previous") channelManager.sendCurrentQueue(queue,"sua-musica-player") } + fun currentMediaIndex(currentMediaIndex:Int) { + Log.i("Player", "Notifying Player Previous") + channelManager.currentMediaIndex("sua-musica-player", currentMediaIndex) + } fun notifyPositionChange(position: Long, duration: Long) { Log.i("Player", "Notifying Player Position change: position: $position duration: $duration") channelManager.notifyPositionChange("sua-musica-player", position, duration) 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 a26ba6f2..6d907ea0 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 @@ -197,7 +197,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { // PlayerSingleton.mediaSessionConnection?.play() // } val position = call.argument(POSITION_ARGUMENT) ?: 0 - PlayerSingleton.mediaSessionConnection?.play( + PlayerSingleton.mediaSessionConnection?.playFromQueue( position ) } diff --git a/packages/player/lib/src/event_type.dart b/packages/player/lib/src/event_type.dart index 250111eb..1fe07b7b 100644 --- a/packages/player/lib/src/event_type.dart +++ b/packages/player/lib/src/event_type.dart @@ -3,6 +3,7 @@ enum EventType { PLAY_NOTIFICATION, BEFORE_PLAY, UPDATE_QUEUE, + SET_CURRENT_MEDIA_INDEX, BUFFERING, PLAYING, PAUSE_REQUEST, diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 153d06b9..710aba7c 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -41,6 +41,8 @@ class Player { bool initializeIsar; bool externalPlayback = false; bool get itemsReady => _queue.itemsReady; + DateTime? _lastPrevious; + static int _queuePosition = 0; CookiesForCustomPolicy? _cookies; PlayerState state = PlayerState.IDLE; @@ -112,6 +114,10 @@ class Player { return Ok; } + static set setQueuePosition(int position) { + _queuePosition = position; + } + Future enqueueAll( List items, { bool shouldRemoveFirst = false, @@ -144,9 +150,24 @@ class Player { ), ) .then((result) => result ?? Future.value(Ok)); + _queue.save(medias: items, saveOnTop: saveOnTop); return Ok; } + List organizeLists( + bool saveOnTop, + List items, + List medias, + ) { + final List topList = saveOnTop ? medias : items; + final List bottomList = saveOnTop ? items : medias; + + return [ + ...topList.toListStringCompressed, + ...bottomList.toListStringCompressed + ]; + } + int removeByPosition( {required List positionsToDelete, required bool isShuffle}) { return _queue.removeByPosition( @@ -154,7 +175,7 @@ class Player { } Future removeAll() async { - _queue.removeAll(); + newQueue.clear(); await IsarService.instance.removeAllMusics(); return Ok; } @@ -186,10 +207,10 @@ class Player { // Duration? duration, // }) async { // if (_queue.size > 0) { - // if (_queue.current == null) { + // if (currentMedia == null) { // _queue.move(0); // } - // final media = _queue.current!; + // final media = currentMedia!; // final data = { // 'albumId': media.albumId.toString(), // 'albumTitle': media.albumTitle, @@ -235,41 +256,50 @@ class Player { } Future restartQueue() async { - final media = _queue.restart(); + // final media = _queue.restart(); + setQueuePosition = 0; - await this.load(media); + // await this.load(media); - return media; + return newQueue[queuePosition]; } Future reorder(int oldIndex, int newIndex, [bool isShuffle = false]) async { - _queue.reorder(oldIndex, newIndex, isShuffle); + // _queue.reorder(oldIndex, newIndex, isShuffle); return Ok; } Future clear() async => removeAll(); - Media? get current => _queue.current; - set current(Media? media) { + set setCurrentMedia(Media? media) { if (media != null) { - _queue.replaceCurrent(media); + newQueue.firstWhere( + (element) => + element.id == media.id && + element.indexInPlaylist == media.indexInPlaylist, + ); + // _queue.replaceCurrent(media); } } static List newQueue = []; + static List? get safeNewQueue => newQueue.isNotEmpty ? newQueue : null; + static Media? get currentMediaStatic => safeNewQueue?[_queuePosition]; + Media? get currentMedia => currentMediaStatic; List get items => newQueue; - int get queuePosition => _queue.index; + int get queuePosition => _queuePosition; + // int get queuePosition => queuePosition; int get previousPlaylistIndex => _queue.previousIndex; PreviousPlaylistPosition? get previousPlaylistPosition => _queue.previousPosition; - int get size => _queue.size; - Media? get top => _queue.top; + int get size => newQueue.length; + Media? get top => safeNewQueue?.first; Future load(Media media) async => _doPlay( - // _queue.current!, + // currentMedia!, // shouldLoadOnly: true, ); @@ -436,8 +466,7 @@ class Player { } Future rewind() async { - var media = _queue.rewind(); - return _rewind(media); + return _rewind(safeNewQueue?[queuePosition]); } Future _rewind(Media? media) async { @@ -449,11 +478,10 @@ class Player { } Future forward() async { - var media = _queue.current; - if (media == null) { + if (currentMedia == null) { return NotOk; } - return _forward(media); + return _forward(currentMedia); } Future _forward(Media? media) async { @@ -466,29 +494,76 @@ class Player { return stop(); } + Media? possiblePrevious() { + if (queuePosition >= 0) { + final now = DateTime.now(); + if (_lastPrevious == null) { + return currentMedia; + } else { + final diff = now.difference(_lastPrevious!).inMilliseconds; + if (diff < 3000) { + var workIndex = queuePosition; + if (queuePosition > 0) { + --workIndex; + } + return safeNewQueue?[workIndex]; + } else { + return currentMedia; + } + } + } + return currentMedia; + } + + Media? possibleNext(RepeatMode repeatMode) { + Media? _next() { + if (newQueue.length == 0) { + return null; + } else if (newQueue.length > 0 && queuePosition < newQueue.length - 1) { + var media = newQueue[queuePosition + 1]; + return media; + } else { + return null; + } + } + + if (repeatMode == RepeatMode.NONE || repeatMode == RepeatMode.TRACK) { + return _next(); + } else if (repeatMode == RepeatMode.QUEUE) { + if (newQueue.length - 1 == queuePosition) { + return safeNewQueue?[0]; + } else { + return _next(); + } + } else { + return null; + } + } + Future previous() async { - Media? media = _queue.possiblePrevious(); + Media? media = possiblePrevious(); if (media == null) { return null; } final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; - final current = _queue.current; - var previous = _queue.previous(); - if (previous == current) { - return _rewind(current); - } else { - _notifyChangeToPrevious(previous); - return _doPlay( - // previous, - // mediaUrl: mediaUrl, - ); - } + //TODO: CRIAR NO NATIVO + // var previous = _queue.previous(); + // if (previous == currentMedia) { + // return _rewind(current); + // } else { + // _notifyChangeToPrevious(previous); + // return _doPlay( + // // previous, + // // mediaUrl: mediaUrl, + // ); + // } } Future next({ bool shallNotify = true, }) async { - final media = _queue.possibleNext(repeatMode); + //TODO: CRIAR NO NATIVO + final media = possibleNext(repeatMode); if (media != null) { final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; @@ -548,16 +623,16 @@ class Player { state = PlayerState.STOPPED; _notifyPlayerStatusChangeEvent(EventType.RELEASED); } - _queue.dispose(); + // _queue.dispose(); return result; } Future shuffle() async { - _queue.shuffle(); + // _queue.shuffle(); } Future unshuffle() async { - _queue.unshuffle(); + // _queue.unshuffle(); } Future seek(Duration position) { @@ -653,7 +728,6 @@ class Player { error, ); break; - case PlayerState.PAUSED: _notifyPlayerStateChangeEvent( player, @@ -707,63 +781,63 @@ class Player { case 'commandCenter.onNext': _log("Player : Command Center : Got a next request"); player.next(); - if (player.current != null) { + if (currentMediaStatic != null) { _addUsingPlayer( player, Event( type: EventType.NEXT_NOTIFICATION, - media: player.current!, - queuePosition: player._queue.index, + media: currentMediaStatic!, + queuePosition: _queuePosition, ), ); } break; case 'commandCenter.onPrevious': _log("Player : Command Center : Got a previous request"); - if (player.current != null) { + if (currentMediaStatic != null) { _addUsingPlayer( player, Event( type: EventType.PREVIOUS_NOTIFICATION, - media: player.current!, - queuePosition: player._queue.index, + media: currentMediaStatic!, + queuePosition: _queuePosition, ), ); } player.previous(); break; case 'commandCenter.onPlay': - if (player.current != null) { + if (currentMediaStatic != null) { _addUsingPlayer( player, Event( type: EventType.PLAY_NOTIFICATION, - media: player.current!, - queuePosition: player._queue.index, + media: currentMediaStatic!, + queuePosition: _queuePosition, ), ); } break; case 'commandCenter.onPause': - if (player.current != null) { + if (currentMediaStatic != null) { _addUsingPlayer( player, Event( type: EventType.PAUSED_NOTIFICATION, - media: player.current!, - queuePosition: player._queue.index, + media: currentMediaStatic!, + queuePosition: _queuePosition, ), ); } break; case 'commandCenter.onTogglePlayPause': - if (player.current != null) { + if (currentMediaStatic != null) { _addUsingPlayer( player, Event( type: EventType.TOGGLE_PLAY_PAUSE, - media: player.current!, - queuePosition: player._queue.index, + media: currentMediaStatic!, + queuePosition: _queuePosition, ), ); } @@ -790,32 +864,35 @@ class Player { "", ); + break; + case 'SET_CURRENT_MEDIA_INDEX': + setQueuePosition = callArgs['CURRENT_MEDIA_INDEX']; + print('#QUEUE_KT PKG ${newQueue[_queuePosition].name}'); + _addUsingPlayer( + player, + Event( + type: EventType.SET_CURRENT_MEDIA_INDEX, + queuePosition: _queuePosition, + media: newQueue[_queuePosition], + ), + ); break; case 'GET_INFO': + // if (callArgs.values.contains('QUEUE_ARGS')) { final queue = callArgs['QUEUE_ARGS']; final parsed = json.decode(queue) as List; - final a = parsed.map((json) => Media.fromJson(json)).toList(); + final queueItems = parsed.map((json) => Media.fromJson(json)).toList(); _addUsingPlayer( player, Event( type: EventType.UPDATE_QUEUE, - queue: a, + queue: queueItems, queuePosition: 0, - media: Media( - id: 0, - albumId: 0, - albumTitle: "0", - name: "0", - ownerId: 0, - author: "0", - url: "0", - isLocal: false, - coverUrl: "0", - bigCoverUrl: "0", - ), + media: queueItems[_queuePosition], ), ); - newQueue.addAll(a); + // newQueue.clear(); + newQueue.addAll(queueItems); break; default: _log('Unknown method ${call.method} '); @@ -823,13 +900,13 @@ class Player { } _notifyChangeToNext(Media media) { - _add( - Event(type: EventType.NEXT, media: media, queuePosition: _queue.index)); + _add(Event( + type: EventType.NEXT, media: media, queuePosition: queuePosition)); } _notifyChangeToPrevious(Media media) { _add(Event( - type: EventType.PREVIOUS, media: media, queuePosition: _queue.index)); + type: EventType.PREVIOUS, media: media, queuePosition: queuePosition)); } _notifyRewind(Media media) async { @@ -838,7 +915,7 @@ class Player { _add(Event( type: EventType.REWIND, media: media, - queuePosition: _queue.index, + queuePosition: queuePosition, position: Duration(milliseconds: positionInMilli), duration: Duration(milliseconds: durationInMilli), )); @@ -851,33 +928,33 @@ class Player { _add(Event( type: EventType.FORWARD, media: media, - queuePosition: _queue.index, + queuePosition: queuePosition, position: Duration(milliseconds: positionInMilli), duration: Duration(milliseconds: durationInMilli), )); } _notifyPlayerStatusChangeEvent(EventType type) { - if (_queue.current != null) { + if (currentMedia != null) { _add(Event( - type: type, media: _queue.current!, queuePosition: _queue.index)); + type: type, media: currentMedia!, queuePosition: queuePosition)); } } _notifyBeforePlayEvent(Function(bool) operation) { _add(BeforePlayEvent( - media: _queue.current!, - queuePosition: _queue.index, + media: currentMedia!, + queuePosition: queuePosition, operation: operation)); } static _notifyDurationChangeEvent(Player player, Duration newDuration) { - if (player._queue.current != null) { + if (currentMediaStatic != null) { _addUsingPlayer( player, DurationChangeEvent( - media: player._queue.current!, - queuePosition: player._queue.index, + media: currentMediaStatic!, + queuePosition: _queuePosition, duration: newDuration)); } } @@ -894,13 +971,13 @@ class Player { errorType: PlayerErrorType.INFORMATION, ); } - if (player._queue.current != null) { + if (currentMediaStatic != null) { _addUsingPlayer( player, Event( type: eventType, - media: player._queue.current!, - queuePosition: player._queue.index, + media: currentMediaStatic!, + queuePosition: _queuePosition, ), ); } @@ -911,13 +988,13 @@ class Player { required String error, PlayerErrorType? errorType, }) { - if (player._queue.current != null) { + if (currentMediaStatic != null) { _addUsingPlayer( player, Event( type: EventType.ERROR_OCCURED, - media: player._queue.current!, - queuePosition: player._queue.index, + media: currentMediaStatic!, + queuePosition: _queuePosition, error: error, errorType: errorType ?? PlayerErrorType.UNDEFINED, ), @@ -927,14 +1004,14 @@ class Player { static _notifyPositionChangeEvent( Player player, Duration newPosition, Duration newDuration) { - final media = player.current; + final media = currentMediaStatic; if (media != null) { final position = newPosition.inSeconds; _addUsingPlayer( player, PositionChangeEvent( media: media, - queuePosition: player.queuePosition, + queuePosition: _queuePosition, position: newPosition, duration: newDuration, ), diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index 10f00cba..f9b3c8b7 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -20,7 +20,7 @@ class Queue { _initialize(); } - Future _initialize() async { + FutureOr>> _initialize() async { if (!itemsReady) { try { final items = await previousItems; @@ -29,10 +29,13 @@ class Queue { int i = 0; storage.addAll(items.map((e) => QueueItem(i++, i, e))); } catch (_) { + return storage; } finally { itemsReady = true; + return storage; } } + return storage; } var _index = 0; @@ -85,37 +88,28 @@ class Queue { return previousPlaylistCurrentIndex?.currentIndex ?? 0; } - int get size => storage.length; - - Media? get top { - if (this.size > 0) { - return storage[0].item; - } - return null; - } - - play(Media media) { - if (storage.length > 0) { - storage.replaceRange(0, 1, [QueueItem(0, 0, media)]); - } else { - int pos = _nextPosition(); - storage.add(QueueItem(pos, pos, media)); - } - _save(medias: [media]); - setIndex = 0; - } - - void replaceCurrent(Media media) { - if (storage.isNotEmpty && index > -1 && index <= (storage.length - 1)) { - storage[index] = storage[index].copyWith(item: media); - } - } - - add(Media media) async { - int pos = _nextPosition(); - storage.add(QueueItem(pos, pos, media)); - await _save(medias: [media]); - } + // play(Media media) { + // if (storage.length > 0) { + // storage.replaceRange(0, 1, [QueueItem(0, 0, media)]); + // } else { + // int pos = _nextPosition(); + // storage.add(QueueItem(pos, pos, media)); + // } + // _save(medias: [media]); + // setIndex = 0; + // } + + // void replaceCurrent(Media media) { + // if (storage.isNotEmpty && index > -1 && index <= (storage.length - 1)) { + // storage[index] = storage[index].copyWith(item: media); + // } + // } + + // add(Media media) async { + // int pos = _nextPosition(); + // storage.add(QueueItem(pos, pos, media)); + // // await _save(medias: [media]); + // } List> _toQueueItems(List items, int i) { return items.map( @@ -140,10 +134,10 @@ class Queue { storage.addAll(_toQueueItems(medias, i)); } - await _save(medias: items, saveOnTop: saveOnTop); + await save(medias: items, saveOnTop: saveOnTop); } - Future _save( + Future save( {required List medias, bool saveOnTop = false}) async { final items = await previousItems; debugPrint( From 66f2535f4a916a1c959840d05b9ceba29cbfd4bc Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 2 Sep 2024 15:34:27 -0300 Subject: [PATCH 20/70] remove sendNotification --- packages/player/lib/src/player.dart | 45 ----------------------------- 1 file changed, 45 deletions(-) diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 710aba7c..ca69c738 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -200,51 +200,6 @@ class Player { return Ok; } - // Future sendNotification({ - // bool? isPlaying, - // bool? isFavorite, - // Duration? position, - // Duration? duration, - // }) async { - // if (_queue.size > 0) { - // if (currentMedia == null) { - // _queue.move(0); - // } - // final media = currentMedia!; - // final data = { - // 'albumId': media.albumId.toString(), - // 'albumTitle': media.albumTitle, - // 'name': media.name, - // 'author': media.author, - // 'url': media.url, - // 'coverUrl': media.coverUrl, - // 'bigCoverUrl': media.bigCoverUrl, - // 'loadOnly': false, - // 'isLocal': media.isLocal, - // }; - - // if (position != null) { - // data['position'] = position.inMilliseconds; - // } - - // if (duration != null) { - // data['duration'] = duration.inMilliseconds; - // } - - // if (isPlaying != null) { - // data['isPlaying'] = isPlaying; - // } - // if (isFavorite != null) { - // data['isFavorite'] = isFavorite; - // } - - // await _invokeMethod('send_notification', data); - // return Ok; - // } else { - // return Ok; - // } - // } - Future disableNotificatonCommands() async { await _invokeMethod('disable_notification_commands'); return Ok; From 28338ddeaac479d28c9de8d76e73caa3ec9332b6 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 2 Sep 2024 17:54:47 -0300 Subject: [PATCH 21/70] remove ununsed methods --- packages/player/android/build.gradle | 2 +- .../player/MediaButtonEventHandler.kt | 58 ++++++------------ .../br/com/suamusica/player/MediaService.kt | 21 +++---- .../player/MediaSessionConnection.kt | 36 ----------- .../br/com/suamusica/player/PlayerPlugin.kt | 59 +++---------------- packages/player/example/lib/sm_player.dart | 6 +- 6 files changed, 39 insertions(+), 143 deletions(-) diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index c3d0a5f0..d376988a 100644 --- a/packages/player/android/build.gradle +++ b/packages/player/android/build.gradle @@ -4,7 +4,7 @@ version '1.0.4' buildscript { ext.kotlin_version = '1.9.20' - ext.media3_version = '1.4.0' + ext.media3_version = '1.4.1' repositories { google() mavenCentral() 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 779c6f9b..9866aa68 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 @@ -42,16 +42,16 @@ class MediaButtonEventHandler( Log.d("Player", "onConnect") val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().apply { - add(SessionCommand("next", Bundle.EMPTY)) + add(SessionCommand("notification_next", Bundle.EMPTY)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - add(SessionCommand("previous", Bundle.EMPTY)) + add(SessionCommand("notification_previous", Bundle.EMPTY)) } add(SessionCommand("notification_favoritar", Bundle.EMPTY)) add(SessionCommand("notification_desfavoritar", Bundle.EMPTY)) add(SessionCommand("seek", session.token.extras)) add(SessionCommand("pause", Bundle.EMPTY)) add(SessionCommand("stop", Bundle.EMPTY)) - add(SessionCommand("next", Bundle.EMPTY)) +// add(SessionCommand("next", Bundle.EMPTY)) add(SessionCommand("enqueue", session.token.extras)) add(SessionCommand("prepare", session.token.extras)) add(SessionCommand("playFromQueue", session.token.extras)) @@ -100,30 +100,22 @@ class MediaButtonEventHandler( if (customCommand.customAction == "stop") { mediaService.stop() } - - if (customCommand.customAction == "send_notification") { - args.let { - val isFavorite = it.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT) - buildIcons(isFavorite) - - } - } if (customCommand.customAction == "play") { mediaService.play() } if (customCommand.customAction == "playFromQueue") { mediaService.playFromQueue(args.getInt(POSITION_ARGUMENT)) } - if (customCommand.customAction == "previous") { + if (customCommand.customAction == "notification_previous") { session.player.seekToPrevious() } - if (customCommand.customAction == "next") { + if (customCommand.customAction == "notification_next") { session.player.seekToNext() } if (customCommand.customAction == "pause") { mediaService.pause() } - if (customCommand.customAction == "ads_playing" || customCommand.customAction == "remove_notification") { + if (customCommand.customAction == "ads_playing" || customCommand.customAction == "remove_notification") { // mediaService.adsPlaying() mediaService.removeNotification() } @@ -140,33 +132,19 @@ class MediaButtonEventHandler( Log.d("Player", "First media item: ${gson.toJson(mediaList.first())}") } - mediaService.enqueue(args.getString("cookie")!!, mediaList, args.getBoolean("autoPlay"),args.getInt("startFromPos")) + mediaService.enqueue( + args.getString("cookie")!!, + mediaList, + args.getBoolean("autoPlay"), + args.getInt("startFromPos") + ) } -// 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) -// } -// } return Futures.immediateFuture( SessionResult(SessionResult.RESULT_SUCCESS) ) } - private fun buildIcons(isFavorite: Boolean): Unit? { + fun buildIcons(isFavorite: Boolean) { val list = ImmutableList.of( CommandButton.Builder() .setDisplayName("Save to favorites") @@ -180,9 +158,9 @@ class MediaButtonEventHandler( .setEnabled(true) .build(), CommandButton.Builder() - .setDisplayName("next") + .setDisplayName("notification_next") .setIconResId(drawable.media3_icon_next) - .setSessionCommand(SessionCommand("next", Bundle.EMPTY)) + .setSessionCommand(SessionCommand("notification_next", Bundle.EMPTY)) .setEnabled(true) .build(), ) @@ -190,9 +168,9 @@ class MediaButtonEventHandler( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { list.plus( CommandButton.Builder() - .setDisplayName("previous") + .setDisplayName("notification_previous") .setIconResId(drawable.media3_icon_previous) - .setSessionCommand(SessionCommand("previous", Bundle.EMPTY)) + .setSessionCommand(SessionCommand("notification_previous", Bundle.EMPTY)) .build() ) } else { @@ -213,7 +191,7 @@ class MediaButtonEventHandler( } @UnstableApi - fun onMediaButtonEventHandler(intent: Intent?,session: MediaSession,) { + fun onMediaButtonEventHandler(intent: Intent?, session: MediaSession) { if (intent == null) { return 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 c94c1f4e..862a98da 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 @@ -39,13 +39,10 @@ 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.DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID import androidx.media3.session.MediaController import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService -import androidx.media3.session.SessionCommand -import androidx.media3.session.SessionResult import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.ListenableFuture @@ -67,7 +64,7 @@ class MediaService : MediaSessionService() { private val userAgent = "SuaMusica/player (Linux; Android ${Build.VERSION.SDK_INT}; ${Build.BRAND}/${Build.MODEL})" - private var media: Media? = null +// private var media: Media? = null private var isForegroundService = false lateinit var mediaSession: MediaSession @@ -222,6 +219,7 @@ class MediaService : MediaSessionService() { if (autoPlay) { play() } + mediaButtonEventHandler.buildIcons(medias[0].isFavorite ?: false) // Use coroutine to prepare and add the remaining media sources CoroutineScope(Dispatchers.Main).launch { for (i in 1 until medias.size) { @@ -239,7 +237,7 @@ class MediaService : MediaSessionService() { } private fun prepare(cookie: String, media: Media): MediaSource { - this.media = media +// this.media = media val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setReadTimeoutMs(15 * 1000) dataSourceFactory.setConnectTimeoutMs(10 * 1000) @@ -280,13 +278,6 @@ class MediaService : MediaSessionService() { private fun buildMetaData(media: Media): MediaMetadata { val metadataBuilder = MediaMetadata.Builder() - - if (media.isFavorite != null) { - mediaSession.sessionExtras.putBoolean( - PlayerPlugin.IS_FAVORITE_ARGUMENT, - media.isFavorite - ) - } val stream = ByteArrayOutputStream() val art = artCache[media.bigCoverUrl] ?: try { @@ -299,6 +290,8 @@ class MediaService : MediaSessionService() { } art?.compress(Bitmap.CompressFormat.PNG, 95, stream) + val bundle = Bundle() + bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, media.isFavorite?:false) metadataBuilder.apply { setAlbumTitle(media.name) setArtist(media.author) @@ -306,6 +299,7 @@ class MediaService : MediaSessionService() { setArtist(media.author) setTitle(media.name) setDisplayTitle(media.name) + setExtras(bundle) } val metadata = metadataBuilder.build() return metadata @@ -418,7 +412,7 @@ class MediaService : MediaSessionService() { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) - PlayerSingleton.playerChangeNotifier?.notifyStateChange(if(isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED) + PlayerSingleton.playerChangeNotifier?.notifyStateChange(if (isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED) } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { @@ -426,6 +420,7 @@ class MediaService : MediaSessionService() { PlayerSingleton.playerChangeNotifier?.currentMediaIndex( player?.currentMediaItemIndex ?: 0 ) + mediaButtonEventHandler.buildIcons(mediaItem?.mediaMetadata?.extras?.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT) ?: false) } override fun onPlaybackStateChanged(playbackState: Int) { 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 8931dd3b..ac48799d 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 @@ -43,21 +43,6 @@ class MediaSessionConnection( } } - fun prepare(cookie: String, media: Media) { - val bundle = Bundle() - bundle.putString("cookie", cookie) - bundle.putString("name", media.name) - bundle.putString("author", media.author) - bundle.putString("url", media.url) - bundle.putString("coverUrl", media.coverUrl) - bundle.putString("bigCoverUrl", media.bigCoverUrl) - - if (media.isFavorite != null) { - bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, media.isFavorite) - } - sendCommand("prepare", bundle) - } - fun enqueue(cookie: String, medias: String, autoPlay:Boolean) { val bundle = Bundle() bundle.putString("cookie", cookie) @@ -116,27 +101,6 @@ class MediaSessionConnection( sendCommand("release", null) } - fun sendNotification( - name: String, - author: String, - url: String, - coverUrl: String, - isPlaying: Boolean?, - bigCoverUrl: String?, - isFavorite: Boolean? - ) { - val bundle = Bundle() - bundle.putString("name", name) - bundle.putString("author", author) - bundle.putString("url", url) - bundle.putString("coverUrl", coverUrl) - bundle.putString("bigCoverUrl", bigCoverUrl) - if (isFavorite != null) { - bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, isFavorite) - } - sendCommand("send_notification", bundle) - } - fun removeNotification() { sendCommand("remove_notification", null) } 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 6d907ea0..eb160bc4 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 @@ -113,64 +113,23 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { "method: ${call.method}" ) when (call.method) { - LOAD_METHOD -> { -// val name = call.argument(NAME_ARGUMENT)!! -// val author = call.argument(AUTHOR_ARGUMENT)!! -// val url = call.argument(URL_ARGUMENT)!! -// val coverUrl = call.argument(COVER_URL_ARGUMENT)!! -// val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) -// val position = call.argument(POSITION_ARGUMENT) -// val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) -// -// PlayerSingleton.mediaSessionConnection?.prepare( -// cookie!!, -// Media(name, author, url, coverUrl, bigCoverUrl, isFavorite) -// ) -// position?.let { -// PlayerSingleton.mediaSessionConnection?.seek(it.toLong(), false) -// } -// PlayerSingleton.mediaSessionConnection?.sendNotification( -// name, -// author, -// url, -// coverUrl, -// null, -// bigCoverUrl, -// isFavorite -// ) -// Log.d(TAG, "method: ${call.method} name: $name author: $author") - } - SEND_NOTIFICATION -> { - val name = call.argument(NAME_ARGUMENT)!! - val author = call.argument(AUTHOR_ARGUMENT)!! - val url = call.argument(URL_ARGUMENT)!! - val coverUrl = call.argument(COVER_URL_ARGUMENT)!! - val isPlaying: Boolean? = call.argument(IS_PLAYING_ARGUMENT) - val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) - val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) - - PlayerSingleton.mediaSessionConnection?.sendNotification( - name, - author, - url, - coverUrl, - isPlaying, - bigCoverUrl, - isFavorite - ) - } - + LOAD_METHOD -> {} "enqueue" -> { val listMedia: List> = call.arguments()!! val arg = listMedia[0] val json = Gson().toJson(listMedia.drop(1)) - PlayerSingleton.mediaSessionConnection?.enqueue(arg["cookie"] as String, json, arg["autoPlay"] as Boolean) + PlayerSingleton.mediaSessionConnection?.enqueue( + arg["cookie"] as String, + json, + arg["autoPlay"] as Boolean + ) } + PLAY_METHOD -> { PlayerSingleton.mediaSessionConnection?.play() } - NEXT_METHOD ->{ + NEXT_METHOD -> { PlayerSingleton.mediaSessionConnection?.next() } @@ -198,7 +157,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { // } val position = call.argument(POSITION_ARGUMENT) ?: 0 PlayerSingleton.mediaSessionConnection?.playFromQueue( - position + position ) } diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index bf514b27..c32374cd 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -168,14 +168,14 @@ class _SMPlayerState extends State { void playOrPause() async { print("Player State: ${_player.state}"); - if (_player.state == PlayerState.IDLE && _player.current != null) { - int result = await _player.play(_player.current!); + if (_player.state == PlayerState.IDLE && _player.currentMedia != null) { + int result = await _player.play(_player.currentMedia!); if (result == Player.Ok) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Audio is now playing!!!!'))); } } else if (_player.state == PlayerState.BUFFERING && - _player.current != null) { + _player.currentMedia != null) { int result = await _player.resume(); if (result == Player.Ok) { ScaffoldMessenger.of(context) From 5e1a8ef4fe69c295c32ed8d0059b0bfcbf19b131 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 2 Sep 2024 17:54:55 -0300 Subject: [PATCH 22/70] remove comment --- .../src/main/kotlin/br/com/suamusica/player/MediaService.kt | 1 - 1 file changed, 1 deletion(-) 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 862a98da..ff17f04f 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 @@ -64,7 +64,6 @@ class MediaService : MediaSessionService() { private val userAgent = "SuaMusica/player (Linux; Android ${Build.VERSION.SDK_INT}; ${Build.BRAND}/${Build.MODEL})" -// private var media: Media? = null private var isForegroundService = false lateinit var mediaSession: MediaSession From d90a1b2bc3c0115b2a7d264a90def1bd284764f4 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 5 Sep 2024 17:35:34 -0300 Subject: [PATCH 23/70] fix shuffle, reorder and remove --- .../player/MediaButtonEventHandler.kt | 110 ++++-- .../br/com/suamusica/player/MediaService.kt | 222 ++++++++--- .../player/MediaSessionConnection.kt | 61 ++- .../suamusica/player/MethodChannelManager.kt | 25 +- .../player/MethodChannelManagerArgsBuilder.kt | 14 + .../suamusica/player/PlayerChangeNotifier.kt | 16 +- .../br/com/suamusica/player/PlayerPlugin.kt | 55 ++- .../br/com/suamusica/player/PlayerState.kt | 3 +- packages/player/example/lib/sm_player.dart | 16 +- packages/player/lib/src/event.dart | 14 +- packages/player/lib/src/event_type.dart | 3 + packages/player/lib/src/player.dart | 367 ++++++++---------- packages/player/lib/src/player_state.dart | 1 + packages/player/lib/src/queue.dart | 5 +- packages/player/lib/src/repeat_mode.dart | 8 +- 15 files changed, 602 insertions(+), 318 deletions(-) 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 9866aa68..d80499ad 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 @@ -7,6 +7,7 @@ import android.util.Log import android.view.KeyEvent import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.Player.COMMAND_GET_TIMELINE import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS @@ -19,7 +20,18 @@ import androidx.media3.session.MediaSession import androidx.media3.session.R.drawable import androidx.media3.session.SessionCommand import androidx.media3.session.SessionResult +import br.com.suamusica.player.PlayerPlugin.Companion.DISABLE_REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE +import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_ONE +import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE +import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.POSITION_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL +import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN +import br.com.suamusica.player.PlayerPlugin.Companion.REORDER +import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE +import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -51,8 +63,16 @@ class MediaButtonEventHandler( add(SessionCommand("seek", session.token.extras)) add(SessionCommand("pause", Bundle.EMPTY)) add(SessionCommand("stop", Bundle.EMPTY)) -// add(SessionCommand("next", Bundle.EMPTY)) - add(SessionCommand("enqueue", session.token.extras)) + add(SessionCommand("next", Bundle.EMPTY)) + add(SessionCommand("previous", Bundle.EMPTY)) + add(SessionCommand(UPDATE_FAVORITE, session.token.extras)) + add(SessionCommand(TOGGLE_SHUFFLE, Bundle.EMPTY)) + add(SessionCommand(REPEAT_MODE, Bundle.EMPTY)) + add(SessionCommand(DISABLE_REPEAT_MODE, Bundle.EMPTY)) + add(SessionCommand(ENQUEUE, session.token.extras)) + add(SessionCommand(REMOVE_ALL, Bundle.EMPTY)) + add(SessionCommand(REORDER, session.token.extras)) + add(SessionCommand(REMOVE_IN, session.token.extras)) add(SessionCommand("prepare", session.token.extras)) add(SessionCommand("playFromQueue", session.token.extras)) add(SessionCommand("play", Bundle.EMPTY)) @@ -68,6 +88,7 @@ class MediaButtonEventHandler( .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) + .add(COMMAND_GET_TIMELINE) .build() return MediaSession.ConnectionResult.AcceptedResultBuilder(session) @@ -88,55 +109,77 @@ class MediaButtonEventHandler( buildIcons(isFavorite) PlayerSingleton.favorite(isFavorite) } - if (customCommand.customAction == "seek") { mediaService.seek(args.getLong("position"), args.getBoolean("playWhenReady")) } - + if (customCommand.customAction == REMOVE_ALL) { + mediaService.removeAll() + } + if (customCommand.customAction == REMOVE_IN) { + mediaService.removeIn(args.getIntegerArrayList(INDEXES_TO_REMOVE)?.toList() ?: emptyList()) + } + if (customCommand.customAction == REORDER) { + val oldIndex = args.getInt("oldIndex") + val newIndex = args.getInt("newIndex") + mediaService.reorder(oldIndex, newIndex) + } if (customCommand.customAction == "onTogglePlayPause") { mediaService.togglePlayPause() } - + if (customCommand.customAction == TOGGLE_SHUFFLE) { + mediaService.toggleShuffle() + } + if (customCommand.customAction == REPEAT_MODE) { + mediaService.repeatMode() + } + if (customCommand.customAction == DISABLE_REPEAT_MODE) { + mediaService.disableRepeatMode() + } if (customCommand.customAction == "stop") { mediaService.stop() } if (customCommand.customAction == "play") { - mediaService.play() + val shouldPrepare = args.getBoolean("shouldPrepare") + mediaService.play(shouldPrepare) } if (customCommand.customAction == "playFromQueue") { mediaService.playFromQueue(args.getInt(POSITION_ARGUMENT)) } - if (customCommand.customAction == "notification_previous") { + if (customCommand.customAction == "notification_previous" || customCommand.customAction == "previous") { session.player.seekToPrevious() } - if (customCommand.customAction == "notification_next") { + if (customCommand.customAction == "notification_next" || customCommand.customAction == "next") { session.player.seekToNext() } if (customCommand.customAction == "pause") { mediaService.pause() } + if (customCommand.customAction == UPDATE_FAVORITE) { + buildIcons(args.getBoolean(IS_FAVORITE_ARGUMENT)) + } if (customCommand.customAction == "ads_playing" || customCommand.customAction == "remove_notification") { // mediaService.adsPlaying() mediaService.removeNotification() } - if (customCommand.customAction == "enqueue") { + if (customCommand.customAction == ENQUEUE || customCommand.customAction == ENQUEUE_ONE) { val json = args.getString("json") // Log the received JSON Log.d("Player", "First media Received JSON for enqueue: $json") val gson = GsonBuilder().create() val mediaListType = object : TypeToken>() {}.type val mediaList: List = gson.fromJson(json, mediaListType) - // Log the first item for debugging if (mediaList.isNotEmpty()) { Log.d("Player", "First media item: ${gson.toJson(mediaList.first())}") } - + buildIcons( + session.player.mediaMetadata.extras?.getBoolean(IS_FAVORITE_ARGUMENT) + ?: false + ) mediaService.enqueue( args.getString("cookie")!!, mediaList, args.getBoolean("autoPlay"), - args.getInt("startFromPos") ) } return Futures.immediateFuture( @@ -145,7 +188,7 @@ class MediaButtonEventHandler( } fun buildIcons(isFavorite: Boolean) { - val list = ImmutableList.of( + val baseList = mutableListOf( CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(if (isFavorite) drawable.media3_icon_heart_filled else drawable.media3_icon_heart_unfilled) @@ -156,27 +199,28 @@ class MediaButtonEventHandler( ) ) .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 - } + .build() ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + baseList.add( + CommandButton.Builder() + .setDisplayName("notification_next") + .setIconResId(drawable.media3_icon_next) + .setSessionCommand(SessionCommand("notification_next", Bundle.EMPTY)) + .setEnabled(true) + .build() + ) + baseList.add( + CommandButton.Builder() + .setDisplayName("notification_previous") + .setIconResId(drawable.media3_icon_previous) + .setSessionCommand(SessionCommand("notification_previous", Bundle.EMPTY)) + .setEnabled(true) + .build() + ) + } + return mediaService.mediaSession.setCustomLayout(baseList) } 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 ff17f04f..329a3ae5 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 @@ -3,16 +3,13 @@ package br.com.suamusica.player import android.app.ActivityManager import android.app.PendingIntent import android.app.Service -import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri -import android.net.wifi.WifiManager import android.os.Build import android.os.Bundle import android.os.Handler -import android.os.PowerManager import android.support.v4.media.session.PlaybackStateCompat import androidx.media3.common.AudioAttributes import androidx.media3.common.C @@ -23,8 +20,11 @@ import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK +import androidx.media3.common.Player.MediaItemTransitionReason +import androidx.media3.common.Player.REPEAT_MODE_ALL +import androidx.media3.common.Player.REPEAT_MODE_OFF +import androidx.media3.common.Player.REPEAT_MODE_ONE import androidx.media3.common.Timeline -import androidx.media3.common.Tracks import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util @@ -36,6 +36,9 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.exoplayer.source.ShuffleOrder +import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder +import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder import androidx.media3.session.CacheBitmapLoader import androidx.media3.session.CommandButton import androidx.media3.session.DefaultMediaNotificationProvider @@ -52,6 +55,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import java.io.File +import java.util.Collections import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -81,10 +85,12 @@ class MediaService : MediaSessionService() { private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader private lateinit var mediaButtonEventHandler: MediaButtonEventHandler - private var customMedia3Notification: MediaNotification? = null + private var shuffleOrder: DefaultShuffleOrder? = null private val artCache = HashMap() - + val queueShuffled = mutableListOf() + var shuffledIndices = mutableListOf() + val queue = mutableListOf() override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) return Service.START_STICKY @@ -127,7 +133,6 @@ class MediaService : MediaSessionService() { .setSessionActivity(getPendingIntent()) .build() this@MediaService.setMediaNotificationProvider(object : MediaNotification.Provider { - override fun createNotification( mediaSession: MediaSession, customLayout: ImmutableList, @@ -135,13 +140,12 @@ class MediaService : MediaSessionService() { onNotificationChangedCallback: MediaNotification.Provider.Callback ): MediaNotification { val defaultMediaNotificationProvider = - DefaultMediaNotificationProvider( - applicationContext, - ).apply { - setSmallIcon(R.drawable.ic_notification) - } + DefaultMediaNotificationProvider(applicationContext) + .apply { + setSmallIcon(R.drawable.ic_notification) + } - customMedia3Notification = + val customMedia3Notification = defaultMediaNotificationProvider.createNotification( mediaSession, mediaSession.customLayout, @@ -151,7 +155,7 @@ class MediaService : MediaSessionService() { return MediaNotification( NOW_PLAYING_NOTIFICATION, - customMedia3Notification!!.notification + customMedia3Notification.notification ) } @@ -160,6 +164,7 @@ class MediaService : MediaSessionService() { action: String, extras: Bundle ): Boolean { + Log.d(TAG, "#MEDIA3# - handleCustomCommand $action") return false } }) @@ -209,14 +214,45 @@ class MediaService : MediaSessionService() { return false } - fun enqueue(cookie: String, medias: List, autoPlay: Boolean, startFromPos: Int) { + fun toggleShuffle() { + player?.shuffleModeEnabled = !(player?.shuffleModeEnabled ?: false) + } + + fun enqueue(cookie: String, medias: List, autoPlay: Boolean) { + var idSum = 0 + if (medias.size == 1) { + queue.add(medias[0]) + if (player?.shuffleModeEnabled == true) { + shuffleOrder?.let { + it.cloneAndInsert(shuffleOrder!!.lastIndex, 1) + player?.setShuffleOrder(it) + for (i in 0 until it.length) { + Log.i( + TAG, + "addMediaSource_one: enqueue: ${player?.getMediaItemAt(i)?.mediaMetadata?.title}" + ) + } + } + player?.setShuffleOrder(shuffleOrder!!) + } + player?.addMediaSource(prepare(cookie, medias[0])) + idSum += medias[0].id + PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias, idSum) + return + } + if (medias.isNotEmpty()) { + queue.clear() + queue.addAll(medias) // Prepare the first media source outside the coroutine - val firstMediaSource = prepare(cookie, medias[0]) - player?.setMediaSource(firstMediaSource) - player?.prepare() - if (autoPlay) { - play() + if (player?.mediaItemCount == 0) { + val firstMediaSource = prepare(cookie, medias[0]) + idSum += medias[0].id + player?.setMediaSource(firstMediaSource) + player?.prepare() + if (autoPlay) { + play() + } } mediaButtonEventHandler.buildIcons(medias[0].isFavorite ?: false) // Use coroutine to prepare and add the remaining media sources @@ -226,17 +262,16 @@ class MediaService : MediaSessionService() { Log.i(TAG, "CoroutineScope: enqueue: ${medias[i].name}") prepare(cookie, medias[i]) } + idSum += medias[i].id player?.addMediaSource(mediaSource) } player?.prepare() - } - PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias) + PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias, idSum) } } private fun prepare(cookie: String, media: Media): MediaSource { -// this.media = media val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setReadTimeoutMs(15 * 1000) dataSourceFactory.setConnectTimeoutMs(10 * 1000) @@ -275,6 +310,52 @@ class MediaService : MediaSessionService() { } } + fun reorder(oldIndex: Int, newIndex: Int) { + player?.moveMediaItem(oldIndex, newIndex) + if (oldIndex < newIndex) { + for (i in oldIndex until newIndex) { + Collections.swap(queue, i, i + 1) + } + } else { + for (i in oldIndex downTo newIndex + 1) { + Collections.swap(queue, i, i - 1) + } + } + } + + fun removeIn(indexes: List) { + if (indexes.isNotEmpty()) { + indexes.forEach { + player?.removeMediaItem(it) + shuffleOrder?.cloneAndRemove(it, it) + shuffledIndices.removeAt(it) + queue.removeAt(it) + } + } + } + + fun disableRepeatMode() { + player?.repeatMode = REPEAT_MODE_OFF + } + + fun repeatMode() { + player?.let { + when (it.repeatMode) { + REPEAT_MODE_OFF -> { + it.repeatMode = REPEAT_MODE_ALL + } + + REPEAT_MODE_ONE -> { + it.repeatMode = REPEAT_MODE_OFF + } + + else -> { + it.repeatMode = REPEAT_MODE_ONE + } + } + } + } + private fun buildMetaData(media: Media): MediaMetadata { val metadataBuilder = MediaMetadata.Builder() val stream = ByteArrayOutputStream() @@ -290,7 +371,7 @@ class MediaService : MediaSessionService() { art?.compress(Bitmap.CompressFormat.PNG, 95, stream) val bundle = Bundle() - bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, media.isFavorite?:false) + bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, media.isFavorite ?: false) metadataBuilder.apply { setAlbumTitle(media.name) setArtist(media.author) @@ -304,20 +385,30 @@ class MediaService : MediaSessionService() { return metadata } - fun play() { + fun play(shouldPrepare: Boolean = false) { performAndEnableTracking { + if (shouldPrepare) { + player?.prepare() + } player?.play() } } fun playFromQueue(position: Int) { - player?.seekTo(position, 0) - PlayerSingleton.playerChangeNotifier?.currentMediaIndex(player?.currentMediaItemIndex ?: 0) + player?.seekTo( + if (player?.shuffleModeEnabled == true) queue.indexOf(queue[shuffledIndices[position]]) else position, + 0 + ) } fun removeNotification() { } + fun removeAll() { + player?.stop() + player?.clearMediaItems() + } + fun seek(position: Long, playWhenReady: Boolean) { player?.seekTo(position) player?.playWhenReady = playWhenReady @@ -395,6 +486,15 @@ class MediaService : MediaSessionService() { stopTrackingProgress() } + fun currentIndex(): Int { + val position = if (player?.shuffleModeEnabled == true) + shuffledIndices.indexOf( + player?.currentMediaItemIndex ?: 0 + ) + else player?.currentMediaItemIndex ?: 0 + return position + } + private fun playerEventListener(): Player.Listener { return object : Player.Listener { override fun onPositionDiscontinuity( @@ -411,15 +511,24 @@ class MediaService : MediaSessionService() { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) - PlayerSingleton.playerChangeNotifier?.notifyStateChange(if (isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED) + if (isPlaying) + PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PLAYING) } - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + override fun onMediaItemTransition( + mediaItem: MediaItem?, + reason: @MediaItemTransitionReason Int + ) { super.onMediaItemTransition(mediaItem, reason) PlayerSingleton.playerChangeNotifier?.currentMediaIndex( - player?.currentMediaItemIndex ?: 0 + currentIndex() + ) + mediaButtonEventHandler.buildIcons( + mediaItem?.mediaMetadata?.extras?.getBoolean( + PlayerPlugin.IS_FAVORITE_ARGUMENT + ) ?: false ) - mediaButtonEventHandler.buildIcons(mediaItem?.mediaMetadata?.extras?.getBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT) ?: false) + PlayerSingleton.playerChangeNotifier?.notifyItemTransition() } override fun onPlaybackStateChanged(playbackState: Int) { @@ -437,10 +546,47 @@ class MediaService : MediaSessionService() { previousState = playbackState } - override fun onRepeatModeChanged(repeatMode: Int) { + override fun onRepeatModeChanged(repeatMode: @Player.RepeatMode Int) { + super.onRepeatModeChanged(repeatMode) + PlayerSingleton.playerChangeNotifier?.onRepeatChanged(repeatMode) } override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + super.onShuffleModeEnabledChanged(shuffleModeEnabled) + if (shuffleModeEnabled) { + val currentIndex = player!!.currentMediaItemIndex + shuffledIndices = (0 until player!!.mediaItemCount).toMutableList() + shuffledIndices.removeAt(currentIndex) + shuffledIndices.shuffle() + shuffledIndices.add(0, currentIndex) + + Log.i(TAG, "Shuffled indices: ${shuffledIndices.joinToString()}") + + queueShuffled.clear() + for (index in shuffledIndices) { + queueShuffled.add(queue[index]) + Log.i(TAG, "Shuffled queue: ${queue[index].name}") + } + + shuffleOrder = DefaultShuffleOrder( + shuffledIndices.toIntArray(), + System.currentTimeMillis() + ) + + player!!.setShuffleOrder(shuffleOrder!!) + } else { + queueShuffled.clear() + queueShuffled.addAll(queue) + } + + PlayerSingleton.playerChangeNotifier?.sendCurrentQueue( + if (shuffleModeEnabled) queueShuffled else queue, + 0 + ) + PlayerSingleton.playerChangeNotifier?.onShuffleModeEnabled(shuffleModeEnabled) + PlayerSingleton.playerChangeNotifier?.currentMediaIndex( + if (shuffleModeEnabled) 0 else player?.currentMediaItemIndex ?: 0 + ) } override fun onPlayerError(error: PlaybackException) { @@ -489,16 +635,4 @@ class MediaService : MediaSessionService() { stopTracking() } } - - private fun stopService() { - if (isForegroundService) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - stopForeground(STOP_FOREGROUND_DETACH) - } else { - stopForeground(false) - } - isForegroundService = false - stopSelf() - } - } } 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 ac48799d..fa55ada3 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt @@ -9,6 +9,17 @@ import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.PlaybackStateCompat import android.util.Log +import br.com.suamusica.player.PlayerPlugin.Companion.DISABLE_REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE +import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_ONE +import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE +import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL +import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN +import br.com.suamusica.player.PlayerPlugin.Companion.REORDER +import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE +import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE import java.lang.ref.WeakReference class MediaSessionConnection( @@ -43,12 +54,12 @@ class MediaSessionConnection( } } - fun enqueue(cookie: String, medias: String, autoPlay:Boolean) { + fun enqueue(cookie: String, medias: String, autoPlay: Boolean) { val bundle = Bundle() bundle.putString("cookie", cookie) bundle.putString("json", medias) bundle.putBoolean("autoPlay", autoPlay) - sendCommand("enqueue", bundle) + sendCommand(ENQUEUE, bundle) } fun playFromQueue(index: Int) { @@ -57,14 +68,19 @@ class MediaSessionConnection( sendCommand("playFromQueue", bundle) } - fun play(index: Int? = null) { + fun play(shouldPrepare: Boolean = false) { val bundle = Bundle() - if (index != null) { - bundle.putInt("index", index) - } + bundle.putBoolean("shouldPrepare", shouldPrepare) sendCommand("play", bundle) } + fun reorder(oldIndex: Int, newIndex: Int) { + val bundle = Bundle() + bundle.putInt("oldIndex", oldIndex) + bundle.putInt("newIndex", newIndex) + sendCommand(REORDER, bundle) + } + fun togglePlayPause() { sendCommand("togglePlayPause", null) } @@ -76,10 +92,43 @@ class MediaSessionConnection( fun pause() { sendCommand("pause", null) } + + fun updateFavorite(isFavorite: Boolean) { + val bundle = Bundle() + bundle.putBoolean(IS_FAVORITE_ARGUMENT, isFavorite) + sendCommand(UPDATE_FAVORITE, bundle) + } + + fun removeAll() { + sendCommand(REMOVE_ALL, null) + } + + fun removeIn(indexes: List) { + val bundle = Bundle() + bundle.putIntegerArrayList(INDEXES_TO_REMOVE, ArrayList(indexes)) + sendCommand(REMOVE_IN, null) + } + fun next() { sendCommand("next", null) } + fun toggleShuffle() { + sendCommand(TOGGLE_SHUFFLE, null) + } + + fun repeatMode() { + sendCommand(REPEAT_MODE, null) + } + + fun disableRepeatMode() { + sendCommand(DISABLE_REPEAT_MODE, null) + } + + fun previous() { + sendCommand("previous", null) + } + fun favorite(shouldFavorite: Boolean) { val bundle = Bundle() bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, shouldFavorite) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt index acc6c566..6b8a7d0a 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt @@ -36,16 +36,24 @@ class MethodChannelManager(private val channel: MethodChannel) { .build() invokeMethod("commandCenter.onPrevious", args) } + fun notifyItemTransition(playerId: String) { + val args = ArgsBuilder() + .playerId(playerId) + .state(PlayerState.ITEM_TRANSITION) + .build() + invokeMethod("state.change", args) + } fun sendCurrentQueue( queue: List, -// currentMediaIndex: Int, + idSum: Int, playerId: String, ) { val args = MethodChannelManagerArgsBuilder() .event("CURRENT_QUEUE") .playerId(playerId) .queue(queue) + .idSum(idSum) .build() invokeMethod("GET_INFO", args) } @@ -57,11 +65,24 @@ class MethodChannelManager(private val channel: MethodChannel) { fun currentMediaIndex(playerId: String, currentMediaIndex: Int) { val args = MethodChannelManagerArgsBuilder() - .event("GET_INFO") .playerId(playerId) .currentMediaIndex(currentMediaIndex) .build() invokeMethod("SET_CURRENT_MEDIA_INDEX", args) } + fun onRepeatChanged(playerId: String, repeatMode: Int) { + val args = MethodChannelManagerArgsBuilder() + .playerId(playerId) + .repeatMode(repeatMode) + .build() + invokeMethod("REPEAT_CHANGED", args) + } + fun onShuffleModeEnabled(playerId: String, shuffleModeEnabled: Boolean) { + val args = MethodChannelManagerArgsBuilder() + .playerId(playerId) + .shuffleModeEnabled(shuffleModeEnabled) + .build() + invokeMethod("SHUFFLE_CHANGED", args) + } } \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt index ec3f56ed..c1164cf0 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt @@ -22,8 +22,22 @@ class MethodChannelManagerArgsBuilder { return this } + fun repeatMode(repeatMode: Int): MethodChannelManagerArgsBuilder { + args["REPEAT_MODE"] = repeatMode + return this + } + + fun shuffleModeEnabled(shuffleModeEnabled: Boolean): MethodChannelManagerArgsBuilder { + args["SHUFFLE_MODE"] = shuffleModeEnabled + return this + } + fun queue(queue: List): MethodChannelManagerArgsBuilder { args["QUEUE_ARGS"] = Gson().toJson(queue) return this } + fun idSum(idSum: Int): MethodChannelManagerArgsBuilder { + args["ID_SUM"] = idSum + return this + } } \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index 4c753707..25250a15 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -31,9 +31,13 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { Log.i("Player", "Notifying Player Previous") channelManager.notifyPrevious("sua-musica-player") } - fun sendCurrentQueue(queue:List) { + fun notifyItemTransition() { + Log.i("Player", "notifyItemTransition") + channelManager.notifyItemTransition("sua-musica-player") + } + fun sendCurrentQueue(queue:List, idSum:Int) { Log.i("Player", "Notifying Player Previous") - channelManager.sendCurrentQueue(queue,"sua-musica-player") + channelManager.sendCurrentQueue(queue,idSum,"sua-musica-player") } fun currentMediaIndex(currentMediaIndex:Int) { Log.i("Player", "Notifying Player Previous") @@ -43,5 +47,13 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { Log.i("Player", "Notifying Player Position change: position: $position duration: $duration") channelManager.notifyPositionChange("sua-musica-player", position, duration) } + fun onRepeatChanged(repeatMode: Int) { + Log.i("Player", "Notifying Player onRepeatChanged: $repeatMode") + channelManager.onRepeatChanged("sua-musica-player", repeatMode) + } + fun onShuffleModeEnabled(shuffleModeEnabled: Boolean) { + Log.i("Player", "Notifying Player onRepeatChanged: $shuffleModeEnabled") + channelManager.onShuffleModeEnabled("sua-musica-player", shuffleModeEnabled) + } } \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt index eb160bc4..e7b7b86f 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 @@ -23,6 +23,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val IS_PLAYING_ARGUMENT = "isPlaying" const val IS_FAVORITE_ARGUMENT = "isFavorite" const val POSITION_ARGUMENT = "position" + const val INDEXES_TO_REMOVE = "indexesToRemove" const val LOAD_ONLY = "loadOnly" const val RELEASE_MODE_ARGUMENT = "releaseMode" private const val CHANNEL = "suamusica.com.br/player" @@ -31,10 +32,20 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { // Method names const val LOAD_METHOD = "load" const val PLAY_METHOD = "play" + const val ENQUEUE = "enqueue" + const val ENQUEUE_ONE = "enqueue_one" + const val REMOVE_ALL = "remove_all" + const val REMOVE_IN = "remove_in" + const val REORDER = "reorder" const val PLAY_FROM_QUEUE_METHOD = "playFromQueue" const val RESUME_METHOD = "resume" const val PAUSE_METHOD = "pause" const val NEXT_METHOD = "next" + const val PREVIOUS_METHOD = "previous" + const val TOGGLE_SHUFFLE = "toggle_shuffle" + const val REPEAT_MODE = "repeat_mode" + const val DISABLE_REPEAT_MODE = "disable_repeat_mode" + const val UPDATE_FAVORITE = "update_favorite" const val STOP_METHOD = "stop" const val RELEASE_METHOD = "release" const val SEEK_METHOD = "seek" @@ -100,7 +111,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { private fun handleMethodCall(call: MethodCall, response: MethodChannel.Result) { val cookie: String? - if (call.method == "enqueue") { + if (call.method == ENQUEUE || call.method == ENQUEUE_ONE) { val listMedia: List> = call.arguments()!! cookie = listMedia[0]["cookie"]!! PlayerSingleton.externalPlayback = listMedia[0]["externalplayback"]!! == "true" @@ -114,7 +125,8 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { ) when (call.method) { LOAD_METHOD -> {} - "enqueue" -> { + ENQUEUE_ONE, + ENQUEUE -> { val listMedia: List> = call.arguments()!! val arg = listMedia[0] val json = Gson().toJson(listMedia.drop(1)) @@ -126,13 +138,50 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { } PLAY_METHOD -> { - PlayerSingleton.mediaSessionConnection?.play() + val shouldPrepare = call.argument("shouldPrepare") ?: false + PlayerSingleton.mediaSessionConnection?.play(shouldPrepare) + } + + REORDER -> { + val from = call.argument("oldIndex")!! + val to = call.argument("newIndex")!! + PlayerSingleton.mediaSessionConnection?.reorder(from, to) + } + + REMOVE_ALL -> { + PlayerSingleton.mediaSessionConnection?.removeAll() + } + + REMOVE_IN -> { + val indexes = call.argument>(INDEXES_TO_REMOVE) ?: emptyList() + PlayerSingleton.mediaSessionConnection?.removeIn(indexes) } NEXT_METHOD -> { PlayerSingleton.mediaSessionConnection?.next() } + TOGGLE_SHUFFLE -> { + PlayerSingleton.mediaSessionConnection?.toggleShuffle() + } + + REPEAT_MODE -> { + PlayerSingleton.mediaSessionConnection?.repeatMode() + } + + DISABLE_REPEAT_MODE -> { + PlayerSingleton.mediaSessionConnection?.disableRepeatMode() + } + + PREVIOUS_METHOD -> { + PlayerSingleton.mediaSessionConnection?.previous() + } + + UPDATE_FAVORITE -> { + val isFavorite = call.argument(IS_FAVORITE_ARGUMENT) ?: false + PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite) + } + PLAY_FROM_QUEUE_METHOD -> { // val name = call.argument(NAME_ARGUMENT)!! // val author = call.argument(AUTHOR_ARGUMENT)!! diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt index 439efa8e..79efe8bb 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt @@ -9,5 +9,6 @@ enum class PlayerState { COMPLETED, ERROR, SEEK_END, - BUFFER_EMPTY + BUFFER_EMPTY, + ITEM_TRANSITION } \ No newline at end of file diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index c32374cd..d1cc7345 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -169,7 +169,7 @@ class _SMPlayerState extends State { print("Player State: ${_player.state}"); if (_player.state == PlayerState.IDLE && _player.currentMedia != null) { - int result = await _player.play(_player.currentMedia!); + int result = await _player.play(); if (result == Player.Ok) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Audio is now playing!!!!'))); @@ -240,9 +240,9 @@ class _SMPlayerState extends State { } _repeatModeToColor() { - if (_player.repeatMode == RepeatMode.NONE) { + if (_player.repeatMode == RepeatMode.REPEAT_MODE_OFF) { return AppColors.black; - } else if (_player.repeatMode == RepeatMode.QUEUE) { + } else if (_player.repeatMode == RepeatMode.REPEAT_MODE_ALL) { return AppColors.primary; } else { return AppColors.darkPink; @@ -250,17 +250,17 @@ class _SMPlayerState extends State { } _changeRepeatMode() { - if (_player.repeatMode == RepeatMode.NONE) { + if (_player.repeatMode == RepeatMode.REPEAT_MODE_OFF) { setState(() { - _player.repeatMode = RepeatMode.QUEUE; + _player.repeatMode = RepeatMode.REPEAT_MODE_ALL; }); - } else if (_player.repeatMode == RepeatMode.QUEUE) { + } else if (_player.repeatMode == RepeatMode.REPEAT_MODE_ALL) { setState(() { - _player.repeatMode = RepeatMode.TRACK; + _player.repeatMode = RepeatMode.REPEAT_MODE_ONE; }); } else { setState(() { - _player.repeatMode = RepeatMode.NONE; + _player.repeatMode = RepeatMode.REPEAT_MODE_OFF; }); } } diff --git a/packages/player/lib/src/event.dart b/packages/player/lib/src/event.dart index eea68866..006854a8 100644 --- a/packages/player/lib/src/event.dart +++ b/packages/player/lib/src/event.dart @@ -1,5 +1,4 @@ -import 'package:smplayer/src/event_type.dart'; -import 'package:smplayer/src/media.dart'; +import 'package:smplayer/player.dart'; class Event { Event({ @@ -11,16 +10,21 @@ class Event { this.errorType, this.position, this.duration, + this.idSum = 0, + this.repeatMode = RepeatMode.REPEAT_MODE_OFF, + this.shuffleEnabled = false, }); final EventType type; final Media media; final String? error; final PlayerErrorType? errorType; - final int queuePosition; + final int queuePosition, idSum; final Duration? position; final Duration? duration; final List? queue; + final RepeatMode repeatMode; + final bool shuffleEnabled; @override String toString() => @@ -34,11 +38,13 @@ class Event { type == other.type && media == other.media && queuePosition == queuePosition && + idSum == idSum && error == other.error && errorType == other.errorType && position == other.position && + queue == other.queue && duration == other.duration; @override - int get hashCode => [type, media, error, errorType, queue].hashCode; + int get hashCode => [type, media, error, errorType, queue, idSum].hashCode; } diff --git a/packages/player/lib/src/event_type.dart b/packages/player/lib/src/event_type.dart index 1fe07b7b..a407f59a 100644 --- a/packages/player/lib/src/event_type.dart +++ b/packages/player/lib/src/event_type.dart @@ -34,6 +34,9 @@ enum EventType { EXTERNAL_PAUSE_REQUESTED, FAVORITE_MUSIC, UNFAVORITE_MUSIC, + ITEM_TRANSITION, + REPEAT_CHANGED, + SHUFFLE_CHANGED, } enum PlayerErrorType { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index ca69c738..8580bc2a 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -47,7 +47,7 @@ class Player { CookiesForCustomPolicy? _cookies; PlayerState state = PlayerState.IDLE; late Queue _queue; - RepeatMode repeatMode = RepeatMode.NONE; + RepeatMode repeatMode = RepeatMode.REPEAT_MODE_OFF; final mutex = Mutex(); final String playerId; @@ -124,19 +124,11 @@ class Player { bool saveOnTop = false, bool autoPlay = false, }) async { - // _queue.addAll( - // items, - // shouldRemoveFirst: shouldRemoveFirst, - // saveOnTop: saveOnTop, - // ); - - //ENQUEUE NO PLAYER - // if(android) _cookies = await cookieSigner(); String cookie = _cookies!.toHeaders(); _channel .invokeMethod( - 'enqueue', + items.length > 1 ? 'enqueue' : 'enqueue_one', items.toListJson ..insert( 0, @@ -150,6 +142,7 @@ class Player { ), ) .then((result) => result ?? Future.value(Ok)); + // _notifyBeforePlayEvent((_) => {}, media: items.first); _queue.save(medias: items, saveOnTop: saveOnTop); return Ok; } @@ -168,15 +161,45 @@ class Player { ]; } - int removeByPosition( - {required List positionsToDelete, required bool isShuffle}) { - return _queue.removeByPosition( - positionsToDelete: positionsToDelete, isShuffle: isShuffle); + int removeByPosition({ + required List positionsToDelete, + }) { + _channel.invokeMethod('remove_in', { + 'indexesToDelete': positionsToDelete, + }); + + return removeByIndexes( + positionsToDelete: positionsToDelete, + ); + } + + int removeByIndexes({ + required List positionsToDelete, + }) { + try { + int lastLength = newQueue.length; + for (var i = 0; i < positionsToDelete.length; ++i) { + final pos = positionsToDelete[i] - i; + if (pos < _queuePosition) { + _queuePosition = _queuePosition - 1; + } + newQueue.removeAt(pos); + } + + // for (var j = 0; j < newQueue.length; ++j) { + // newQueue[j].position = j; + // } + + return lastLength - newQueue.length; + } catch (e) { + return 0; + } } Future removeAll() async { newQueue.clear(); await IsarService.instance.removeAllMusics(); + _channel.invokeMethod('remove_all'); return Ok; } @@ -200,16 +223,6 @@ class Player { return Ok; } - Future disableNotificatonCommands() async { - await _invokeMethod('disable_notification_commands'); - return Ok; - } - - Future enableNotificatonCommands() async { - await _invokeMethod('enable_notification_commands'); - return Ok; - } - Future restartQueue() async { // final media = _queue.restart(); setQueuePosition = 0; @@ -222,9 +235,30 @@ class Player { Future reorder(int oldIndex, int newIndex, [bool isShuffle = false]) async { // _queue.reorder(oldIndex, newIndex, isShuffle); + _reorder(oldIndex, newIndex); + _channel.invokeMethod('reorder', { + 'oldIndex': oldIndex, + 'newIndex': newIndex, + }); return Ok; } + void _reorder(int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + for (int i = oldIndex; i < newIndex; i++) { + final temp = newQueue[i]; + newQueue[i] = newQueue[i + 1]; + newQueue[i + 1] = temp; + } + } else { + for (int i = oldIndex; i > newIndex; i--) { + final temp = newQueue[i]; + newQueue[i] = newQueue[i - 1]; + newQueue[i - 1] = temp; + } + } + } + Future clear() async => removeAll(); set setCurrentMedia(Media? media) { @@ -253,21 +287,9 @@ class Player { int get size => newQueue.length; Media? get top => safeNewQueue?.first; - Future load(Media media) async => _doPlay( - // currentMedia!, - // shouldLoadOnly: true, - ); - - Future play( - Media media, { - double volume = 1.0, - Duration? position, - bool respectSilence = false, - bool stayAwake = false, - }) async { - // _queue.play(media); + Future play({bool shouldPrepare = false}) async { _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); - return _doPlay(); + return _doPlay(shouldPrepare: shouldPrepare); } Future playFromQueue( @@ -279,133 +301,22 @@ class Player { bool shallNotify = false, bool loadOnly = false, }) async { - // Media? media = _queue.item(pos); - // if (media != null) { - // final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; if (!loadOnly) { _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); } - // return _doPlay( - // _queue.move(pos)!, - // shallNotify: shallNotify, - // mediaUrl: mediaUrl, - // shouldLoadOnly: loadOnly, - // position: position, - // ); - // } else { - // return NotOk; - // } return _channel.invokeMethod( 'playFromQueue', {'position': pos}).then((result) => result); - // _channel - // .invokeMethod( - // 'enqueue', - // items.toListJson - // ..insert( - // 0, - // { - // 'playerId': playerId, - // 'cookie': cookie, - // 'shallSendEvents': _shallSendEvents, - // 'externalplayback': externalPlayback, - // }, - // ), - // ) - // .then((result) => result ?? Future.value(Ok)); - } - - Future _doPlay( - // Media media, { - // double? volume, - // Duration? position, - // bool? respectSilence, - // bool? stayAwake, - // bool? shallNotify, - // bool? shouldLoadOnly, - // String? mediaUrl, - // } - ) async { - // volume ??= 1.0; - // respectSilence ??= false; - // stayAwake ??= false; - // shallNotify ??= false; - // shouldLoadOnly ??= false; - - // if (shallNotify) { - // _notifyChangeToNext(media); - // } - // mediaUrl ??= (await localMediaValidator?.call(media)) ?? media.url; - // //If it is local, check if it exists before playing it. - - // if (!mediaUrl.startsWith("http")) { - // if (!File(mediaUrl).existsSync() && media.fallbackUrl != null) { - // //Should we remove from DB?? - // mediaUrl = media.fallbackUrl; - // } - // } - - // // we need to update the value as it could have been - // // downloading and is not downloaded - // media.isLocal = !mediaUrl!.startsWith("http"); - // media.url = mediaUrl; - // if (shouldLoadOnly) { - // debugPrint("LOADING ONLY!"); - // return invokeLoad({ - // 'albumId': media.albumId.toString(), - // 'albumTitle': media.albumTitle, - // 'name': media.name, - // 'author': media.author, - // 'url': mediaUrl, - // 'coverUrl': media.coverUrl, - // 'bigCoverUrl': media.bigCoverUrl, - // 'loadOnly': true, - // 'isLocal': media.isLocal, - // 'volume': volume, - // 'position': position?.inMilliseconds, - // 'respectSilence': respectSilence, - // 'stayAwake': stayAwake, - // 'isFavorite': media.isFavorite - // }); - // } else if (autoPlay) { - // _notifyBeforePlayEvent((loadOnly) => {}); - - // return invokePlay(media, { - // 'albumId': media.albumId.toString(), - // 'albumTitle': media.albumTitle, - // 'name': media.name, - // 'author': media.author, - // 'url': mediaUrl, - // 'coverUrl': media.coverUrl, - // 'bigCoverUrl': media.bigCoverUrl, - // 'loadOnly': false, - // 'isLocal': media.isLocal, - // 'volume': volume, - // 'position': position?.inMilliseconds, - // 'respectSilence': respectSilence, - // 'stayAwake': stayAwake, - // 'isFavorite': media.isFavorite - // }); - // } else { - // _notifyBeforePlayEvent((loadOnly) { - // invokePlay(media, { - // 'albumId': media.albumId.toString(), - // 'albumTitle': media.albumTitle, - // 'name': media.name, - // 'author': media.author, - // 'url': mediaUrl, - // 'coverUrl': media.coverUrl, - // 'bigCoverUrl': media.bigCoverUrl, - // 'loadOnly': loadOnly, - // 'isLocal': media.isLocal, - // 'volume': volume, - // 'position': position?.inMilliseconds, - // 'respectSilence': respectSilence, - // 'stayAwake': stayAwake, - // 'isFavorite': media.isFavorite - // }); - // }); - _channel.invokeMethod('play'); + } + Future _doPlay({ + bool shouldPrepare = false, + }) async { + _notifyBeforePlayEvent( + (_) => _channel.invokeMethod( + 'play', + {'shouldPrepare': shouldPrepare}, + ), + ); return Ok; } @@ -453,6 +364,7 @@ class Player { if (queuePosition >= 0) { final now = DateTime.now(); if (_lastPrevious == null) { + _lastPrevious = now; return currentMedia; } else { final diff = now.difference(_lastPrevious!).inMilliseconds; @@ -470,6 +382,16 @@ class Player { return currentMedia; } + Future toggleRepeatMode() async { + return _channel.invokeMethod('repeat_mode').then((result) => result); + } + + Future disableRepeatMode() async { + return _channel + .invokeMethod('disable_repeat_mode') + .then((result) => result); + } + Media? possibleNext(RepeatMode repeatMode) { Media? _next() { if (newQueue.length == 0) { @@ -482,9 +404,10 @@ class Player { } } - if (repeatMode == RepeatMode.NONE || repeatMode == RepeatMode.TRACK) { + if (repeatMode == RepeatMode.REPEAT_MODE_OFF || + repeatMode == RepeatMode.REPEAT_MODE_ONE) { return _next(); - } else if (repeatMode == RepeatMode.QUEUE) { + } else if (repeatMode == RepeatMode.REPEAT_MODE_ALL) { if (newQueue.length - 1 == queuePosition) { return safeNewQueue?[0]; } else { @@ -500,24 +423,13 @@ class Player { if (media == null) { return null; } - final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; - //TODO: CRIAR NO NATIVO - // var previous = _queue.previous(); - // if (previous == currentMedia) { - // return _rewind(current); - // } else { - // _notifyChangeToPrevious(previous); - // return _doPlay( - // // previous, - // // mediaUrl: mediaUrl, - // ); - // } + // final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; + _channel.invokeMethod('previous').then((result) => result); } Future next({ bool shallNotify = true, }) async { - //TODO: CRIAR NO NATIVO final media = possibleNext(repeatMode); if (media != null) { final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; @@ -538,6 +450,13 @@ class Player { return _channel.invokeMethod('next').then((result) => result); } + Future updateFavorite({ + required bool isFavorite, + }) async { + return _channel.invokeMethod( + 'update_favorite', {'isFavorite': isFavorite}).then((result) => result); + } + Future pause() async { _notifyPlayerStatusChangeEvent(EventType.PAUSE_REQUEST); @@ -561,7 +480,7 @@ class Player { Future resume() async { _notifyPlayerStatusChangeEvent(EventType.RESUME_REQUESTED); - final int result = await _invokeMethod('resume'); + final int result = await _invokeMethod('play'); if (result == Ok) { _notifyPlayerStatusChangeEvent(EventType.RESUMED); @@ -584,10 +503,12 @@ class Player { Future shuffle() async { // _queue.shuffle(); + _channel.invokeMethod('toggle_shuffle'); } Future unshuffle() async { // _queue.unshuffle(); + _channel.invokeMethod('toggle_shuffle'); } Future seek(Duration position) { @@ -615,24 +536,25 @@ class Player { } } - static Future _handleOnComplete(Player player) async { - player.state = PlayerState.COMPLETED; - _notifyPlayerStateChangeEvent(player, EventType.FINISHED_PLAYING, ""); - switch (player.repeatMode) { - case RepeatMode.NONE: - case RepeatMode.QUEUE: - player._doNext(shallNotify: false); - break; + // static Future _handleOnComplete(Player player) async { + // player.state = PlayerState.COMPLETED; + // _notifyPlayerStateChangeEvent(player, EventType.FINISHED_PLAYING, ""); + // switch (player.repeatMode) { + // case RepeatMode.REPEAT_MODE_OFF: + // case RepeatMode.REPEAT_MODE_ALL: + // player._doNext(shallNotify: false); + // break; - case RepeatMode.TRACK: - player.rewind(); - break; - } - } + // case RepeatMode.REPEAT_MODE_ONE: + // player.rewind(); + // break; + // } + // } static Future _doHandlePlatformCall(MethodCall call) async { final Map callArgs = call.arguments as Map; _log('_platformCallHandler call ${call.method} $callArgs'); + _log('_platformCallHandler1 call ${call.method}'); switch (call.method) { case 'audio.onDuration': final duration = callArgs['duration']; @@ -648,9 +570,6 @@ class Player { Duration newDuration = Duration(milliseconds: duration); _notifyPositionChangeEvent(player, newPosition, newDuration); break; - case 'audio.onComplete': - _handleOnComplete(player); - break; case 'audio.onError': player.state = PlayerState.ERROR; final errorType = callArgs['errorType'] ?? 2; @@ -664,8 +583,8 @@ class Player { case 'state.change': final state = callArgs['state']; String error = callArgs['error'] ?? ""; + _log('state.change1 call ${PlayerState.values[state]}'); player.state = PlayerState.values[state]; - switch (player.state) { case PlayerState.IDLE: break; @@ -676,6 +595,16 @@ class Player { error, ); break; + case PlayerState.ITEM_TRANSITION: + _addUsingPlayer( + player, + BeforePlayEvent( + media: newQueue[_queuePosition], + queuePosition: _queuePosition, + operation: (bool _) {}, + ), + ); + break; case PlayerState.PLAYING: _notifyPlayerStateChangeEvent( player, @@ -716,7 +645,7 @@ class Player { break; case PlayerState.COMPLETED: - _handleOnComplete(player); + // _handleOnComplete(player); break; case PlayerState.ERROR: @@ -832,38 +761,55 @@ class Player { ), ); break; + case 'REPEAT_CHANGED': + final repeatMode = RepeatMode.values[callArgs['REPEAT_MODE']]; + _addUsingPlayer( + player, + Event( + type: EventType.REPEAT_CHANGED, + queuePosition: _queuePosition, + media: newQueue[_queuePosition], + repeatMode: repeatMode, + ), + ); + break; + case 'SHUFFLE_CHANGED': + final shuffleEnabled = callArgs['SHUFFLE_MODE']; + _addUsingPlayer( + player, + Event( + type: EventType.SHUFFLE_CHANGED, + queuePosition: _queuePosition, + media: newQueue[_queuePosition], + shuffleEnabled: shuffleEnabled, + ), + ); + break; case 'GET_INFO': // if (callArgs.values.contains('QUEUE_ARGS')) { final queue = callArgs['QUEUE_ARGS']; + final idSum = callArgs['ID_SUM']; final parsed = json.decode(queue) as List; final queueItems = parsed.map((json) => Media.fromJson(json)).toList(); + newQueue.clear(); + newQueue.addAll(queueItems); _addUsingPlayer( player, Event( type: EventType.UPDATE_QUEUE, queue: queueItems, queuePosition: 0, + idSum: idSum, media: queueItems[_queuePosition], ), ); - // newQueue.clear(); - newQueue.addAll(queueItems); + break; default: _log('Unknown method ${call.method} '); } } - _notifyChangeToNext(Media media) { - _add(Event( - type: EventType.NEXT, media: media, queuePosition: queuePosition)); - } - - _notifyChangeToPrevious(Media media) { - _add(Event( - type: EventType.PREVIOUS, media: media, queuePosition: queuePosition)); - } - _notifyRewind(Media media) async { final positionInMilli = await getCurrentPosition(); final durationInMilli = await getDuration(); @@ -896,11 +842,14 @@ class Player { } } - _notifyBeforePlayEvent(Function(bool) operation) { - _add(BeforePlayEvent( - media: currentMedia!, + _notifyBeforePlayEvent(Function(bool) operation, {Media? media}) { + _add( + BeforePlayEvent( + media: media ?? currentMedia!, queuePosition: queuePosition, - operation: operation)); + operation: operation, + ), + ); } static _notifyDurationChangeEvent(Player player, Duration newDuration) { diff --git a/packages/player/lib/src/player_state.dart b/packages/player/lib/src/player_state.dart index b977d867..96896623 100644 --- a/packages/player/lib/src/player_state.dart +++ b/packages/player/lib/src/player_state.dart @@ -8,4 +8,5 @@ enum PlayerState { ERROR, SEEK_END, BUFFER_EMPTY, + ITEM_TRANSITION, } diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index f9b3c8b7..8abd6faa 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -296,9 +296,10 @@ class Queue { } Media? possibleNext(RepeatMode repeatMode) { - if (repeatMode == RepeatMode.NONE || repeatMode == RepeatMode.TRACK) { + if (repeatMode == RepeatMode.REPEAT_MODE_OFF || + repeatMode == RepeatMode.REPEAT_MODE_ONE) { return _next(); - } else if (repeatMode == RepeatMode.QUEUE) { + } else if (repeatMode == RepeatMode.REPEAT_MODE_ALL) { if (storage.length - 1 == index) { return storage[0].item; } else { diff --git a/packages/player/lib/src/repeat_mode.dart b/packages/player/lib/src/repeat_mode.dart index 0df9667b..da1d5114 100644 --- a/packages/player/lib/src/repeat_mode.dart +++ b/packages/player/lib/src/repeat_mode.dart @@ -1,13 +1,13 @@ -enum RepeatMode { NONE, QUEUE, TRACK } +enum RepeatMode { REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL } extension ParseToString on RepeatMode { String toShortString() { switch (this) { - case RepeatMode.NONE: + case RepeatMode.REPEAT_MODE_OFF: return 'Disabled'; - case RepeatMode.QUEUE: + case RepeatMode.REPEAT_MODE_ALL: return 'Queue'; - case RepeatMode.TRACK: + case RepeatMode.REPEAT_MODE_ONE: return 'Track'; default: return "Unknown"; From 05b8d0bc1b49bd5d09f9ff68bae395587e4329fe Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 6 Sep 2024 10:49:15 -0300 Subject: [PATCH 24/70] daily --- .../br/com/suamusica/player/MediaService.kt | 1 + packages/player/lib/src/media.dart | 8 + packages/player/lib/src/player.dart | 19 +- packages/player/lib/src/queue.dart | 306 +----------------- 4 files changed, 21 insertions(+), 313 deletions(-) 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 329a3ae5..4c219467 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 @@ -237,6 +237,7 @@ class MediaService : MediaSessionService() { } player?.addMediaSource(prepare(cookie, medias[0])) idSum += medias[0].id + player?.prepare() PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias, idSum) return } diff --git a/packages/player/lib/src/media.dart b/packages/player/lib/src/media.dart index ce6be301..b4bbaf09 100644 --- a/packages/player/lib/src/media.dart +++ b/packages/player/lib/src/media.dart @@ -196,6 +196,14 @@ extension ListMediaToListStringCompressed on List { List get toListStringCompressed => map((e) => e.toString()).toList(); List> get toListJson => map((e) => e.toJson()).toList(); + + // List get toQueueItemData => map((e) { + // return QueueItemData( + // position: e.indexInPlaylist ?? 0, + // media: e, + // markedForDelete: false, + // ); + // }).toList(); } extension ListStringToListPlayable on List { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 8580bc2a..f75e7379 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -46,7 +46,7 @@ class Player { CookiesForCustomPolicy? _cookies; PlayerState state = PlayerState.IDLE; - late Queue _queue; + static late Queue _queue; RepeatMode repeatMode = RepeatMode.REPEAT_MODE_OFF; final mutex = Mutex(); @@ -272,20 +272,18 @@ class Player { } } - static List newQueue = []; + static List newQueue = _queue.storage.map((e) => e.item).toList(); static List? get safeNewQueue => newQueue.isNotEmpty ? newQueue : null; static Media? get currentMediaStatic => safeNewQueue?[_queuePosition]; Media? get currentMedia => currentMediaStatic; - List get items => newQueue; int get queuePosition => _queuePosition; - // int get queuePosition => queuePosition; int get previousPlaylistIndex => _queue.previousIndex; PreviousPlaylistPosition? get previousPlaylistPosition => _queue.previousPosition; + List get items => newQueue; int get size => newQueue.length; - Media? get top => safeNewQueue?.first; Future play({bool shouldPrepare = false}) async { _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); @@ -311,11 +309,12 @@ class Player { Future _doPlay({ bool shouldPrepare = false, }) async { - _notifyBeforePlayEvent( - (_) => _channel.invokeMethod( - 'play', - {'shouldPrepare': shouldPrepare}, - ), + // _notifyBeforePlayEvent( + // (_) => + _channel.invokeMethod( + 'play', + {'shouldPrepare': shouldPrepare}, + // ), ); return Ok; } diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index 8abd6faa..e257a6bf 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -5,22 +5,17 @@ import 'package:smplayer/src/isar_service.dart'; import 'package:smplayer/src/media.dart'; import 'package:smplayer/src/previous_playlist_model.dart'; import 'package:smplayer/src/queue_item.dart'; -import 'package:smplayer/src/repeat_mode.dart'; -import 'package:smplayer/src/shuffler.dart'; -import 'package:smplayer/src/simple_shuffle.dart'; class Queue { Queue({ - shuffler, - mode, this.initializeIsar = false, - }) : _shuffler = shuffler ?? SimpleShuffler() { + }) { IsarService.instance.isarEnabled = initializeIsar; itemsReady = !initializeIsar; _initialize(); } - FutureOr>> _initialize() async { + Future _initialize() async { if (!itemsReady) { try { final items = await previousItems; @@ -29,38 +24,18 @@ class Queue { int i = 0; storage.addAll(items.map((e) => QueueItem(i++, i, e))); } catch (_) { - return storage; } finally { itemsReady = true; - return storage; } } - return storage; } - var _index = 0; - int get index => _index; - Media? _current; - - set setIndex(int index) { - if (storage.isNotEmpty && index >= 0 && index <= storage.length - 1) { - _index = index; - _current = storage[index].item; - } - } - - final Shuffler _shuffler; final bool initializeIsar; bool itemsReady = false; int previousIndex = 0; PreviousPlaylistPosition? previousPosition; var storage = >[]; PreviousPlaylistMusics? previousPlaylistMusics; - DateTime? _lastPrevious; - - Media? get current => - _current ?? - (index >= 0 && index < storage.length ? storage[index].item : null); List get items { return storage.length > 0 @@ -88,52 +63,11 @@ class Queue { return previousPlaylistCurrentIndex?.currentIndex ?? 0; } - // play(Media media) { - // if (storage.length > 0) { - // storage.replaceRange(0, 1, [QueueItem(0, 0, media)]); - // } else { - // int pos = _nextPosition(); - // storage.add(QueueItem(pos, pos, media)); - // } - // _save(medias: [media]); - // setIndex = 0; - // } - - // void replaceCurrent(Media media) { - // if (storage.isNotEmpty && index > -1 && index <= (storage.length - 1)) { - // storage[index] = storage[index].copyWith(item: media); - // } - // } - - // add(Media media) async { - // int pos = _nextPosition(); - // storage.add(QueueItem(pos, pos, media)); - // // await _save(medias: [media]); - // } - - List> _toQueueItems(List items, int i) { - return items.map( - (e) { - i++; - return QueueItem(i, i, e); - }, - ).toList(); - } - addAll( List items, { bool shouldRemoveFirst = false, bool saveOnTop = false, }) async { - final medias = shouldRemoveFirst ? items.sublist(1) : items; - - int i = storage.length == 1 ? 0 : storage.length - 1; - if (saveOnTop) { - storage.insertAll(0, _toQueueItems(medias, i)); - } else { - storage.addAll(_toQueueItems(medias, i)); - } - await save(medias: items, saveOnTop: saveOnTop); } @@ -162,241 +96,7 @@ class Queue { ...bottomList.toListStringCompressed ]; } - - int removeByPosition( - {required List positionsToDelete, required bool isShuffle}) { - try { - int lastLength = storage.length; - for (var i = 0; i < positionsToDelete.length; ++i) { - final pos = positionsToDelete[i] - i; - if (pos < index) { - setIndex = index - 1; - } - storage.removeAt(pos); - } - - for (var j = 0; j < storage.length; ++j) { - storage[j].position = j; - } - - if (kDebugMode) { - for (var e in storage) { - debugPrint( - '=====> storage remove: ${e.item.name} - ${e.position} | ${e.originalPosition}'); - } - } - return lastLength - storage.length; - } catch (e) { - return 0; - } - } - - clear() => removeAll(); - - removeAll() { - storage.clear(); - setIndex = 0; - } - - shuffle() { - if (storage.length > 2) { - var current = storage[index]; - _shuffler.shuffle(storage); - for (var i = 0; i < storage.length; ++i) { - storage[i].position = i; - } - var currentIndex = storage.indexOf(current); - reorder(currentIndex, 0, true); - setIndex = 0; - } - } - - unshuffle() { - if (storage.length > 2) { - var current = storage[index]; - _shuffler.unshuffle(storage); - for (var i = 0; i < storage.length; ++i) { - final item = storage[i]; - item.position = i; - } - if (kDebugMode) { - for (var e in storage) { - debugPrint( - '=====> storage unshuffle: ${e.item.name} - ${e.position} | ${e.originalPosition}'); - } - } - setIndex = current.position; - } - } - - _nextPosition() { - if (storage.length == 0) return 0; - return storage.length; - } - - Media rewind() { - assert(index >= 0 && index < storage.length); - return storage[index].item; - } - - Media previous() { - assert(index >= 0); - final now = DateTime.now(); - if (_lastPrevious == null) { - _lastPrevious = now; - return rewind(); - } else { - final diff = now.difference(_lastPrevious!).inMilliseconds; - print("diff: $diff"); - if (diff < 3000) { - if (index > 0) { - setIndex = index - 1; - } - return storage[index].item; - } else { - _lastPrevious = now; - return rewind(); - } - } - } - - Media? possiblePrevious() { - if (index >= 0) { - final now = DateTime.now(); - if (_lastPrevious == null) { - return storage[index].item; - } else { - final diff = now.difference(_lastPrevious!).inMilliseconds; - if (diff < 3000) { - var workIndex = index; - if (index > 0) { - --workIndex; - } - return storage[workIndex].item; - } else { - return storage[index].item; - } - } - } - return storage.isNotEmpty && index >= 0 ? storage[index].item : null; - } - - Media? next() { - if (storage.length == 0) { - throw AssertionError("Queue is empty"); - } else if (storage.length > 0 && index < storage.length - 1) { - final newIndex = index + 1; - setIndex = newIndex; - var media = storage[newIndex].item; - _updateIndex(media.id, newIndex); - return media; - } else { - return null; - } - } - - Media? possibleNext(RepeatMode repeatMode) { - if (repeatMode == RepeatMode.REPEAT_MODE_OFF || - repeatMode == RepeatMode.REPEAT_MODE_ONE) { - return _next(); - } else if (repeatMode == RepeatMode.REPEAT_MODE_ALL) { - if (storage.length - 1 == index) { - return storage[0].item; - } else { - return _next(); - } - } else { - return null; - } - } - - Media? _next() { - if (storage.length == 0) { - return null; - } else if (storage.length > 0 && index < storage.length - 1) { - var media = storage[index + 1].item; - return media; - } else { - return null; - } - } - - Media? move(int pos) { - if (storage.length == 0) { - throw AssertionError("Queue is empty"); - } else if (storage.length > 0 && pos <= storage.length - 1) { - var media = storage[pos].item; - setIndex = pos; - return media; - } else { - return null; - } - } - - void _updateIndex(int id, int newIndex) async { - IsarService.instance.addPreviousPlaylistCurrentIndex( - PreviousPlaylistCurrentIndex(mediaId: id, currentIndex: newIndex), - ); - } - - Media? item(int pos) { - final item = storage[pos].item; - _updateIndex(item.id, pos); - if (storage.length == 0) { - return null; - } else if (storage.length > 0 && pos <= storage.length - 1) { - return item; - } else { - return null; - } - } - - Media restart() { - setIndex = 0; - return storage.first.item; - } - - void reorder(int oldIndex, int newIndex, [bool isShuffle = false]) { - final playingItem = storage.elementAt(index); - if (newIndex > oldIndex) { - for (int i = oldIndex + 1; i <= newIndex; i++) { - if (!isShuffle) { - storage[i].originalPosition--; - } - storage[i].position--; - } - } else { - for (int i = newIndex; i < oldIndex; i++) { - if (!isShuffle) { - storage[i].originalPosition++; - } - storage[i].position++; - } - } - - storage[oldIndex].position = newIndex; - if (!isShuffle) { - storage[oldIndex].originalPosition = newIndex; - } - storage.sort((a, b) => a.position.compareTo(b.position)); - final playingIndex = storage.indexOf(playingItem); - - if (kDebugMode) { - debugPrint( - '=====> ${storage[oldIndex].item.name} - storage[oldIndex]: ${storage[oldIndex].originalPosition}', - ); - debugPrint( - '=====> ${storage[newIndex].item.name} - storage[newIndex]: ${storage[newIndex].originalPosition}', - ); - for (var e in storage) { - debugPrint( - '=====> storage Reorder: ${e.item.name} - ${e.position} - ${e.originalPosition}', - ); - } - } - - setIndex = playingIndex; - } + // } void dispose() { IsarService.instance.dispose(); From a50f177aaee116ccfa8215b3f84a66a6e1170020 Mon Sep 17 00:00:00 2001 From: Alan Trope Date: Fri, 6 Sep 2024 13:07:54 -0300 Subject: [PATCH 25/70] chore: daily --- .../main/kotlin/br/com/suamusica/player/MediaService.kt | 7 ++++++- packages/player/lib/src/isar_service.dart | 8 ++++---- packages/player/lib/src/player.dart | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) 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 4c219467..b1109933 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 @@ -512,8 +512,12 @@ class MediaService : MediaSessionService() { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) - if (isPlaying) + if (isPlaying){ PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PLAYING) + startTrackingProgress() + } else{ + stopTrackingProgressAndPerformTask {} + } } override fun onMediaItemTransition( @@ -535,6 +539,7 @@ class MediaService : MediaSessionService() { override fun onPlaybackStateChanged(playbackState: Int) { super.onPlaybackStateChanged(playbackState) PlayerSingleton.playerChangeNotifier?.notifyStateChange(playbackState) + if (playbackState == Player.STATE_READY) { if (previousState == -1) { notifyPositionChange() diff --git a/packages/player/lib/src/isar_service.dart b/packages/player/lib/src/isar_service.dart index 6d055342..804e1aa4 100644 --- a/packages/player/lib/src/isar_service.dart +++ b/packages/player/lib/src/isar_service.dart @@ -63,7 +63,7 @@ class IsarService { await _isarStorage?.previousPlaylistMusics .put(previousPlaylistMusics); }, - silent: !kDebugMode, + silent: kDebugMode, ); } catch (_) {} } @@ -83,7 +83,7 @@ class IsarService { await _isarStorage?.previousPlaylistCurrentIndexs .put(previousPlaylistCurrentIndex); }, - silent: !kDebugMode, + silent: kDebugMode, ); } catch (_) {} } @@ -104,7 +104,7 @@ class IsarService { await _isarStorage?.previousPlaylistPositions .put(previousPlaylistPosition); }, - silent: !kDebugMode, + silent: kDebugMode, ); } catch (_) {} } @@ -118,6 +118,6 @@ class IsarService { () async { await _isarStorage?.clear(); }, - silent: !kDebugMode, + silent: kDebugMode, ); } diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index f75e7379..afc30857 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -272,7 +272,7 @@ class Player { } } - static List newQueue = _queue.storage.map((e) => e.item).toList(); + static List get newQueue => _queue.storage.map((e) => e.item).toList(); static List? get safeNewQueue => newQueue.isNotEmpty ? newQueue : null; static Media? get currentMediaStatic => safeNewQueue?[_queuePosition]; Media? get currentMedia => currentMediaStatic; From 5505e3729c41f4a1327ab546fbd22ed32ddf35d3 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 9 Sep 2024 16:55:12 -0300 Subject: [PATCH 26/70] favorite and isar ok --- .../player/MediaButtonEventHandler.kt | 75 +++- .../br/com/suamusica/player/MediaService.kt | 155 +++----- .../player/MediaSessionConnection.kt | 17 +- .../br/com/suamusica/player/PlayerPlugin.kt | 8 +- packages/player/lib/src/player.dart | 374 +++++++----------- packages/player/lib/src/queue.dart | 313 ++++++++++++++- 6 files changed, 582 insertions(+), 360 deletions(-) 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 d80499ad..68ff37ca 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 @@ -23,8 +23,11 @@ import androidx.media3.session.SessionResult import br.com.suamusica.player.PlayerPlugin.Companion.DISABLE_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_ONE +import br.com.suamusica.player.PlayerPlugin.Companion.FAVORITE +import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST import br.com.suamusica.player.PlayerPlugin.Companion.POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN @@ -66,6 +69,7 @@ class MediaButtonEventHandler( add(SessionCommand("next", Bundle.EMPTY)) add(SessionCommand("previous", Bundle.EMPTY)) add(SessionCommand(UPDATE_FAVORITE, session.token.extras)) + add(SessionCommand(FAVORITE, session.token.extras)) add(SessionCommand(TOGGLE_SHUFFLE, Bundle.EMPTY)) add(SessionCommand(REPEAT_MODE, Bundle.EMPTY)) add(SessionCommand(DISABLE_REPEAT_MODE, Bundle.EMPTY)) @@ -106,17 +110,30 @@ class MediaButtonEventHandler( 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) + buildIcons() } + if (customCommand.customAction == "seek") { mediaService.seek(args.getLong("position"), args.getBoolean("playWhenReady")) } + if (customCommand.customAction == FAVORITE) { + val mediaItem = session.player.currentMediaItem!! + updateFavoriteMetadata( + session.player, + session.player.currentMediaItemIndex, + mediaItem, + args.getBoolean(IS_FAVORITE_ARGUMENT) + ) + buildIcons() + } if (customCommand.customAction == REMOVE_ALL) { mediaService.removeAll() } if (customCommand.customAction == REMOVE_IN) { - mediaService.removeIn(args.getIntegerArrayList(INDEXES_TO_REMOVE)?.toList() ?: emptyList()) + mediaService.removeIn( + args.getIntegerArrayList(INDEXES_TO_REMOVE)?.toList() ?: emptyList() + ) } if (customCommand.customAction == REORDER) { val oldIndex = args.getInt("oldIndex") @@ -127,7 +144,12 @@ class MediaButtonEventHandler( mediaService.togglePlayPause() } if (customCommand.customAction == TOGGLE_SHUFFLE) { - mediaService.toggleShuffle() +// val list = args.getSerializable("list",ArrayList>()::class.java) + val json = args.getString(POSITIONS_LIST) + val gson = GsonBuilder().create() + val mediaListType = object : TypeToken>>() {}.type + val positionsList: List> = gson.fromJson(json, mediaListType) + mediaService.toggleShuffle(positionsList) } if (customCommand.customAction == REPEAT_MODE) { mediaService.repeatMode() @@ -155,7 +177,22 @@ class MediaButtonEventHandler( mediaService.pause() } if (customCommand.customAction == UPDATE_FAVORITE) { - buildIcons(args.getBoolean(IS_FAVORITE_ARGUMENT)) + val isFavorite = args.getBoolean(IS_FAVORITE_ARGUMENT) + val id = args.getInt(ID_FAVORITE_ARGUMENT) + session.player.let { + for (i in 0 until it.mediaItemCount) { + val mediaItem = it.getMediaItemAt(i) + if (mediaItem.mediaId == id.toString()) { + updateFavoriteMetadata(it, i, mediaItem, isFavorite) + if (id.toString() == session.player.currentMediaItem?.mediaId) { + buildIcons() + } + break + } + } + } + PlayerSingleton.favorite(true) +// } } if (customCommand.customAction == "ads_playing" || customCommand.customAction == "remove_notification") { // mediaService.adsPlaying() @@ -172,10 +209,7 @@ class MediaButtonEventHandler( if (mediaList.isNotEmpty()) { Log.d("Player", "First media item: ${gson.toJson(mediaList.first())}") } - buildIcons( - session.player.mediaMetadata.extras?.getBoolean(IS_FAVORITE_ARGUMENT) - ?: false - ) + buildIcons() mediaService.enqueue( args.getString("cookie")!!, mediaList, @@ -187,7 +221,30 @@ class MediaButtonEventHandler( ) } - fun buildIcons(isFavorite: Boolean) { + private fun updateFavoriteMetadata( + player: Player, + i: Int, + mediaItem: MediaItem, + isFavorite: Boolean + ) { + player.replaceMediaItem( + i, + mediaItem.buildUpon().setMediaMetadata( + mediaItem.mediaMetadata.buildUpon().setExtras( + Bundle().apply { + putBoolean(IS_FAVORITE_ARGUMENT, isFavorite) + } + ).build() + ).build() + ) + } + + fun buildIcons() { + val isFavorite = + mediaService.player?.currentMediaItem?.mediaMetadata?.extras?.getBoolean( + IS_FAVORITE_ARGUMENT + ) ?: false + val baseList = mutableListOf( CommandButton.Builder() .setDisplayName("Save to favorites") 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 b1109933..23b4eb29 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 @@ -24,7 +24,6 @@ import androidx.media3.common.Player.MediaItemTransitionReason import androidx.media3.common.Player.REPEAT_MODE_ALL import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Player.REPEAT_MODE_ONE -import androidx.media3.common.Timeline import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util @@ -38,7 +37,6 @@ import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ShuffleOrder import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder -import androidx.media3.exoplayer.source.ShuffleOrder.UnshuffledShuffleOrder import androidx.media3.session.CacheBitmapLoader import androidx.media3.session.CommandButton import androidx.media3.session.DefaultMediaNotificationProvider @@ -77,20 +75,19 @@ class MediaService : MediaSessionService() { AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA) .build() - private var player: ExoPlayer? = null + var player: ExoPlayer? = null private var progressTracker: ProgressTracker? = null - private var previousState: Int = -1 - private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader private lateinit var mediaButtonEventHandler: MediaButtonEventHandler private var shuffleOrder: DefaultShuffleOrder? = null private val artCache = HashMap() - val queueShuffled = mutableListOf() var shuffledIndices = mutableListOf() - val queue = mutableListOf() + + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) return Service.START_STICKY @@ -214,37 +211,47 @@ class MediaService : MediaSessionService() { return false } - fun toggleShuffle() { + fun toggleShuffle(positionsList: List>) { player?.shuffleModeEnabled = !(player?.shuffleModeEnabled ?: false) + player?.shuffleModeEnabled?.let { + if (it) { + shuffledIndices.clear() + for (element in positionsList) { + shuffledIndices.add(element["originalPosition"] ?: 0) + } + shuffleOrder = DefaultShuffleOrder( + shuffledIndices.toIntArray(), + System.currentTimeMillis() + ) + player!!.setShuffleOrder(shuffleOrder!!) + } + PlayerSingleton.playerChangeNotifier?.onShuffleModeEnabled(it) + } } fun enqueue(cookie: String, medias: List, autoPlay: Boolean) { var idSum = 0 - if (medias.size == 1) { - queue.add(medias[0]) - if (player?.shuffleModeEnabled == true) { - shuffleOrder?.let { - it.cloneAndInsert(shuffleOrder!!.lastIndex, 1) - player?.setShuffleOrder(it) - for (i in 0 until it.length) { - Log.i( - TAG, - "addMediaSource_one: enqueue: ${player?.getMediaItemAt(i)?.mediaMetadata?.title}" - ) - } - } - player?.setShuffleOrder(shuffleOrder!!) - } - player?.addMediaSource(prepare(cookie, medias[0])) - idSum += medias[0].id - player?.prepare() - PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias, idSum) - return - } +// if (medias.size == 1 && (player?.mediaItemCount ?: 0) > 0) { +// if (player?.shuffleModeEnabled == true) { +// shuffleOrder?.let { +// it.cloneAndInsert(shuffleOrder!!.lastIndex, 1) +// player?.setShuffleOrder(it) +// for (i in 0 until it.length) { +// Log.i( +// TAG, +// "addMediaSource_one: enqueue: ${player?.getMediaItemAt(i)?.mediaMetadata?.title}" +// ) +// } +// } +// player?.setShuffleOrder(shuffleOrder!!) +// } +// player?.addMediaSource(prepare(cookie, medias[0])) +// idSum += medias[0].id +// PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias, idSum) +// return +// } if (medias.isNotEmpty()) { - queue.clear() - queue.addAll(medias) // Prepare the first media source outside the coroutine if (player?.mediaItemCount == 0) { val firstMediaSource = prepare(cookie, medias[0]) @@ -252,10 +259,10 @@ class MediaService : MediaSessionService() { player?.setMediaSource(firstMediaSource) player?.prepare() if (autoPlay) { - play() + player?.playWhenReady = true } } - mediaButtonEventHandler.buildIcons(medias[0].isFavorite ?: false) +// mediaButtonEventHandler.buildIcons(medias[0].isFavorite ?: false) // Use coroutine to prepare and add the remaining media sources CoroutineScope(Dispatchers.Main).launch { for (i in 1 until medias.size) { @@ -268,7 +275,7 @@ class MediaService : MediaSessionService() { } player?.prepare() } - PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias, idSum) + PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(listOf(medias[0]), idSum) } } @@ -282,7 +289,8 @@ class MediaService : MediaSessionService() { val metadata = buildMetaData(media) val url = media.url val uri = if (url.startsWith("/")) Uri.fromFile(File(url)) else Uri.parse(url) - val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata).build() + val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata) + .setMediaId(media.id.toString()).build() @C.ContentType val type = Util.inferContentType(uri) return when (type) { @@ -313,24 +321,18 @@ class MediaService : MediaSessionService() { fun reorder(oldIndex: Int, newIndex: Int) { player?.moveMediaItem(oldIndex, newIndex) - if (oldIndex < newIndex) { - for (i in oldIndex until newIndex) { - Collections.swap(queue, i, i + 1) - } - } else { - for (i in oldIndex downTo newIndex + 1) { - Collections.swap(queue, i, i - 1) - } - } + PlayerSingleton.playerChangeNotifier?.currentMediaIndex( + currentIndex() + ) } + fun removeIn(indexes: List) { if (indexes.isNotEmpty()) { indexes.forEach { player?.removeMediaItem(it) shuffleOrder?.cloneAndRemove(it, it) shuffledIndices.removeAt(it) - queue.removeAt(it) } } } @@ -397,7 +399,7 @@ class MediaService : MediaSessionService() { fun playFromQueue(position: Int) { player?.seekTo( - if (player?.shuffleModeEnabled == true) queue.indexOf(queue[shuffledIndices[position]]) else position, + if (player?.shuffleModeEnabled == true) shuffledIndices[position] else position, 0 ) } @@ -493,6 +495,7 @@ class MediaService : MediaSessionService() { player?.currentMediaItemIndex ?: 0 ) else player?.currentMediaItemIndex ?: 0 +// return player?.currentMediaItemIndex ?: 0 return position } @@ -512,10 +515,10 @@ class MediaService : MediaSessionService() { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) - if (isPlaying){ + if (isPlaying) { PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PLAYING) startTrackingProgress() - } else{ + } else { stopTrackingProgressAndPerformTask {} } } @@ -528,28 +531,16 @@ class MediaService : MediaSessionService() { PlayerSingleton.playerChangeNotifier?.currentMediaIndex( currentIndex() ) - mediaButtonEventHandler.buildIcons( - mediaItem?.mediaMetadata?.extras?.getBoolean( - PlayerPlugin.IS_FAVORITE_ARGUMENT - ) ?: false - ) + mediaButtonEventHandler.buildIcons() PlayerSingleton.playerChangeNotifier?.notifyItemTransition() } - override fun onPlaybackStateChanged(playbackState: Int) { + override fun onPlaybackStateChanged(playbackState: @Player.State Int) { super.onPlaybackStateChanged(playbackState) PlayerSingleton.playerChangeNotifier?.notifyStateChange(playbackState) - - if (playbackState == Player.STATE_READY) { - if (previousState == -1) { - notifyPositionChange() - } else { - stopTrackingProgressAndPerformTask {} - } - } else if (playbackState == Player.STATE_ENDED) { + if (playbackState == Player.STATE_ENDED) { stopTrackingProgressAndPerformTask {} } - previousState = playbackState } override fun onRepeatModeChanged(repeatMode: @Player.RepeatMode Int) { @@ -557,44 +548,6 @@ class MediaService : MediaSessionService() { PlayerSingleton.playerChangeNotifier?.onRepeatChanged(repeatMode) } - override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { - super.onShuffleModeEnabledChanged(shuffleModeEnabled) - if (shuffleModeEnabled) { - val currentIndex = player!!.currentMediaItemIndex - shuffledIndices = (0 until player!!.mediaItemCount).toMutableList() - shuffledIndices.removeAt(currentIndex) - shuffledIndices.shuffle() - shuffledIndices.add(0, currentIndex) - - Log.i(TAG, "Shuffled indices: ${shuffledIndices.joinToString()}") - - queueShuffled.clear() - for (index in shuffledIndices) { - queueShuffled.add(queue[index]) - Log.i(TAG, "Shuffled queue: ${queue[index].name}") - } - - shuffleOrder = DefaultShuffleOrder( - shuffledIndices.toIntArray(), - System.currentTimeMillis() - ) - - player!!.setShuffleOrder(shuffleOrder!!) - } else { - queueShuffled.clear() - queueShuffled.addAll(queue) - } - - PlayerSingleton.playerChangeNotifier?.sendCurrentQueue( - if (shuffleModeEnabled) queueShuffled else queue, - 0 - ) - PlayerSingleton.playerChangeNotifier?.onShuffleModeEnabled(shuffleModeEnabled) - PlayerSingleton.playerChangeNotifier?.currentMediaIndex( - if (shuffleModeEnabled) 0 else player?.currentMediaItemIndex ?: 0 - ) - } - override fun onPlayerError(error: PlaybackException) { val bundle = Bundle() bundle.putString("type", "error") 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 fa55ada3..36238062 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 @@ -12,14 +12,17 @@ import android.util.Log import br.com.suamusica.player.PlayerPlugin.Companion.DISABLE_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_ONE +import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN import br.com.suamusica.player.PlayerPlugin.Companion.REORDER import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE +import com.google.gson.Gson import java.lang.ref.WeakReference class MediaSessionConnection( @@ -93,9 +96,10 @@ class MediaSessionConnection( sendCommand("pause", null) } - fun updateFavorite(isFavorite: Boolean) { + fun updateFavorite(isFavorite: Boolean, idFavorite: Int) { val bundle = Bundle() bundle.putBoolean(IS_FAVORITE_ARGUMENT, isFavorite) + bundle.putInt(ID_FAVORITE_ARGUMENT, idFavorite) sendCommand(UPDATE_FAVORITE, bundle) } @@ -113,8 +117,13 @@ class MediaSessionConnection( sendCommand("next", null) } - fun toggleShuffle() { - sendCommand(TOGGLE_SHUFFLE, null) + fun toggleShuffle(positionsList: List>) { + val bundle = Bundle() + //API33 +// bundle.putSerializable(POSITIONS_LIST, ArrayList(positionsList)) + val json = Gson().toJson(positionsList) + bundle.putString(POSITIONS_LIST, json) + sendCommand(TOGGLE_SHUFFLE, bundle) } fun repeatMode() { @@ -131,7 +140,7 @@ class MediaSessionConnection( fun favorite(shouldFavorite: Boolean) { val bundle = Bundle() - bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, shouldFavorite) + bundle.putBoolean(IS_FAVORITE_ARGUMENT, shouldFavorite) sendCommand(PlayerPlugin.FAVORITE, bundle) } 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 e7b7b86f..aee86002 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 @@ -22,8 +22,10 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val BIG_COVER_URL_ARGUMENT = "bigCoverUrl" const val IS_PLAYING_ARGUMENT = "isPlaying" const val IS_FAVORITE_ARGUMENT = "isFavorite" + const val ID_FAVORITE_ARGUMENT = "idFavorite" const val POSITION_ARGUMENT = "position" const val INDEXES_TO_REMOVE = "indexesToRemove" + const val POSITIONS_LIST = "positionsList" const val LOAD_ONLY = "loadOnly" const val RELEASE_MODE_ARGUMENT = "releaseMode" private const val CHANNEL = "suamusica.com.br/player" @@ -162,7 +164,8 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { } TOGGLE_SHUFFLE -> { - PlayerSingleton.mediaSessionConnection?.toggleShuffle() + val positionsList = call.argument>>(POSITIONS_LIST) ?: emptyList() + PlayerSingleton.mediaSessionConnection?.toggleShuffle(positionsList) } REPEAT_MODE -> { @@ -179,7 +182,8 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { UPDATE_FAVORITE -> { val isFavorite = call.argument(IS_FAVORITE_ARGUMENT) ?: false - PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite) + val idFavorite = call.argument(ID_FAVORITE_ARGUMENT) ?: 0 + PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite,idFavorite) } PLAY_FROM_QUEUE_METHOD -> { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index afc30857..59b95de7 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:smaws/aws.dart'; import 'package:flutter/services.dart'; -import 'package:smplayer/src/before_play_event.dart'; import 'package:smplayer/src/event.dart'; import 'package:smplayer/src/event_type.dart'; import 'package:smplayer/src/isar_service.dart'; @@ -41,17 +40,23 @@ class Player { bool initializeIsar; bool externalPlayback = false; bool get itemsReady => _queue.itemsReady; - DateTime? _lastPrevious; - static int _queuePosition = 0; CookiesForCustomPolicy? _cookies; PlayerState state = PlayerState.IDLE; static late Queue _queue; - RepeatMode repeatMode = RepeatMode.REPEAT_MODE_OFF; + static RepeatMode _repeatMode = RepeatMode.REPEAT_MODE_OFF; + static bool _shuffleEnabled = false; + static int idSum = 0; final mutex = Mutex(); final String playerId; + bool get isShuffleEnabled => _shuffleEnabled; + RepeatMode get repeatMode => _repeatMode; + + set setShuffleEnabled(bool value) => _shuffleEnabled = value; + set repeatMode(RepeatMode value) => _repeatMode = value; + final StreamController _eventStreamController = StreamController(); @@ -114,8 +119,8 @@ class Player { return Ok; } - static set setQueuePosition(int position) { - _queuePosition = position; + set setQueuePosition(int position) { + _queue.setIndex = position; } Future enqueueAll( @@ -142,8 +147,9 @@ class Player { ), ) .then((result) => result ?? Future.value(Ok)); - // _notifyBeforePlayEvent((_) => {}, media: items.first); - _queue.save(medias: items, saveOnTop: saveOnTop); + + await IsarService.instance.removeAllMusics(); + _queue.addAll(items); return Ok; } @@ -164,50 +170,22 @@ class Player { int removeByPosition({ required List positionsToDelete, }) { - _channel.invokeMethod('remove_in', { - 'indexesToDelete': positionsToDelete, - }); + _channel.invokeMethod('remove_in', {'indexesToDelete': positionsToDelete}); - return removeByIndexes( + return _queue.removeByPosition( positionsToDelete: positionsToDelete, + isShuffle: isShuffleEnabled, ); } - int removeByIndexes({ - required List positionsToDelete, - }) { - try { - int lastLength = newQueue.length; - for (var i = 0; i < positionsToDelete.length; ++i) { - final pos = positionsToDelete[i] - i; - if (pos < _queuePosition) { - _queuePosition = _queuePosition - 1; - } - newQueue.removeAt(pos); - } - - // for (var j = 0; j < newQueue.length; ++j) { - // newQueue[j].position = j; - // } - - return lastLength - newQueue.length; - } catch (e) { - return 0; - } - } - Future removeAll() async { - newQueue.clear(); + _queue.clear(); + setQueuePosition = 0; await IsarService.instance.removeAllMusics(); _channel.invokeMethod('remove_all'); return Ok; } - // Future removeNotificaton() async { - // await _invokeMethod('remove_notification'); - // return Ok; - // } - Future adsPlaying() async { await _invokeMethod('ads_playing'); return Ok; @@ -224,18 +202,18 @@ class Player { } Future restartQueue() async { - // final media = _queue.restart(); - setQueuePosition = 0; - - // await this.load(media); + final media = _queue.restart(); - return newQueue[queuePosition]; + return media; } - Future reorder(int oldIndex, int newIndex, - [bool isShuffle = false]) async { + Future reorder( + int oldIndex, + int newIndex, + ) async { // _queue.reorder(oldIndex, newIndex, isShuffle); - _reorder(oldIndex, newIndex); + final result = _queue.reorder(oldIndex, newIndex, isShuffleEnabled); + debugPrint('#_queue.reorder: ${getPositionsList()}'); _channel.invokeMethod('reorder', { 'oldIndex': oldIndex, 'newIndex': newIndex, @@ -243,47 +221,19 @@ class Player { return Ok; } - void _reorder(int oldIndex, int newIndex) { - if (oldIndex < newIndex) { - for (int i = oldIndex; i < newIndex; i++) { - final temp = newQueue[i]; - newQueue[i] = newQueue[i + 1]; - newQueue[i + 1] = temp; - } - } else { - for (int i = oldIndex; i > newIndex; i--) { - final temp = newQueue[i]; - newQueue[i] = newQueue[i - 1]; - newQueue[i - 1] = temp; - } - } - } - Future clear() async => removeAll(); - set setCurrentMedia(Media? media) { - if (media != null) { - newQueue.firstWhere( - (element) => - element.id == media.id && - element.indexInPlaylist == media.indexInPlaylist, - ); - // _queue.replaceCurrent(media); - } - } - - static List get newQueue => _queue.storage.map((e) => e.item).toList(); - static List? get safeNewQueue => newQueue.isNotEmpty ? newQueue : null; - static Media? get currentMediaStatic => safeNewQueue?[_queuePosition]; - Media? get currentMedia => currentMediaStatic; + Media? get currentMedia => _queue.current; - int get queuePosition => _queuePosition; int get previousPlaylistIndex => _queue.previousIndex; PreviousPlaylistPosition? get previousPlaylistPosition => _queue.previousPosition; - List get items => newQueue; - int get size => newQueue.length; + List get items => _queue.items; + List get isarItems => _queue.storage.map((e) => e.item).toList(); + int get size => items.length; + + int get currentIndex => _queue.index; Future play({bool shouldPrepare = false}) async { _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); @@ -331,15 +281,18 @@ class Player { } Future rewind() async { - return _rewind(safeNewQueue?[queuePosition]); + final media = _queue.rewind(); + _notifyRewind(media); + return Ok; } - Future _rewind(Media? media) async { - if (media == null) { - return NotOk; - } - _notifyRewind(media); - return player.externalPlayback ? 1 : await seek(Duration(seconds: 0)); + List> getPositionsList() { + return [ + for (var item in _queue.storage) + { + 'originalPosition': item.originalPosition, + } + ]; } Future forward() async { @@ -359,28 +312,6 @@ class Player { return stop(); } - Media? possiblePrevious() { - if (queuePosition >= 0) { - final now = DateTime.now(); - if (_lastPrevious == null) { - _lastPrevious = now; - return currentMedia; - } else { - final diff = now.difference(_lastPrevious!).inMilliseconds; - if (diff < 3000) { - var workIndex = queuePosition; - if (queuePosition > 0) { - --workIndex; - } - return safeNewQueue?[workIndex]; - } else { - return currentMedia; - } - } - } - return currentMedia; - } - Future toggleRepeatMode() async { return _channel.invokeMethod('repeat_mode').then((result) => result); } @@ -391,45 +322,20 @@ class Player { .then((result) => result); } - Media? possibleNext(RepeatMode repeatMode) { - Media? _next() { - if (newQueue.length == 0) { - return null; - } else if (newQueue.length > 0 && queuePosition < newQueue.length - 1) { - var media = newQueue[queuePosition + 1]; - return media; - } else { - return null; - } - } - - if (repeatMode == RepeatMode.REPEAT_MODE_OFF || - repeatMode == RepeatMode.REPEAT_MODE_ONE) { - return _next(); - } else if (repeatMode == RepeatMode.REPEAT_MODE_ALL) { - if (newQueue.length - 1 == queuePosition) { - return safeNewQueue?[0]; - } else { - return _next(); - } - } else { - return null; - } - } - Future previous() async { - Media? media = possiblePrevious(); + Media? media = _queue.possibleNext(repeatMode); if (media == null) { return null; } // final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; _channel.invokeMethod('previous').then((result) => result); + return Ok; } Future next({ bool shallNotify = true, }) async { - final media = possibleNext(repeatMode); + final media = _queue.possibleNext(repeatMode); if (media != null) { final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; @@ -451,9 +357,12 @@ class Player { Future updateFavorite({ required bool isFavorite, + required int id, }) async { - return _channel.invokeMethod( - 'update_favorite', {'isFavorite': isFavorite}).then((result) => result); + return _channel.invokeMethod('update_favorite', { + 'isFavorite': isFavorite, + 'idFavorite': id, + }).then((result) => result); } Future pause() async { @@ -500,14 +409,15 @@ class Player { return result; } - Future shuffle() async { - // _queue.shuffle(); - _channel.invokeMethod('toggle_shuffle'); - } - - Future unshuffle() async { - // _queue.unshuffle(); - _channel.invokeMethod('toggle_shuffle'); + Future toggleShuffle() async { + if (!isShuffleEnabled) { + _queue.shuffle(); + } else { + _queue.unshuffle(); + } + debugPrint('#_queue.shuffle: ${getPositionsList()}'); + _channel + .invokeMethod('toggle_shuffle', {'positionsList': getPositionsList()}); } Future seek(Duration position) { @@ -551,9 +461,12 @@ class Player { // } static Future _doHandlePlatformCall(MethodCall call) async { + final currentMedia = _queue.current; + final currentIndex = _queue.index; final Map callArgs = call.arguments as Map; - _log('_platformCallHandler call ${call.method} $callArgs'); - _log('_platformCallHandler1 call ${call.method}'); + if (call.method != 'audio.onCurrentPosition') { + _log('_platformCallHandler call ${call.method} $callArgs'); + } switch (call.method) { case 'audio.onDuration': final duration = callArgs['duration']; @@ -582,7 +495,7 @@ class Player { case 'state.change': final state = callArgs['state']; String error = callArgs['error'] ?? ""; - _log('state.change1 call ${PlayerState.values[state]}'); + _log('state.change call ${PlayerState.values[state]}'); player.state = PlayerState.values[state]; switch (player.state) { case PlayerState.IDLE: @@ -595,13 +508,10 @@ class Player { ); break; case PlayerState.ITEM_TRANSITION: - _addUsingPlayer( + _notifyPlayerStateChangeEvent( player, - BeforePlayEvent( - media: newQueue[_queuePosition], - queuePosition: _queuePosition, - operation: (bool _) {}, - ), + EventType.BEFORE_PLAY, + error, ); break; case PlayerState.PLAYING: @@ -664,63 +574,63 @@ class Player { case 'commandCenter.onNext': _log("Player : Command Center : Got a next request"); player.next(); - if (currentMediaStatic != null) { + if (currentMedia != null) { _addUsingPlayer( player, Event( type: EventType.NEXT_NOTIFICATION, - media: currentMediaStatic!, - queuePosition: _queuePosition, + media: currentMedia, + queuePosition: currentIndex, ), ); } break; case 'commandCenter.onPrevious': _log("Player : Command Center : Got a previous request"); - if (currentMediaStatic != null) { + if (currentMedia != null) { _addUsingPlayer( player, Event( type: EventType.PREVIOUS_NOTIFICATION, - media: currentMediaStatic!, - queuePosition: _queuePosition, + media: currentMedia, + queuePosition: currentIndex, ), ); } player.previous(); break; case 'commandCenter.onPlay': - if (currentMediaStatic != null) { + if (currentMedia != null) { _addUsingPlayer( player, Event( type: EventType.PLAY_NOTIFICATION, - media: currentMediaStatic!, - queuePosition: _queuePosition, + media: currentMedia, + queuePosition: currentIndex, ), ); } break; case 'commandCenter.onPause': - if (currentMediaStatic != null) { + if (currentMedia != null) { _addUsingPlayer( player, Event( type: EventType.PAUSED_NOTIFICATION, - media: currentMediaStatic!, - queuePosition: _queuePosition, + media: currentMedia, + queuePosition: currentIndex, ), ); } break; case 'commandCenter.onTogglePlayPause': - if (currentMediaStatic != null) { + if (currentMedia != null) { _addUsingPlayer( player, Event( type: EventType.TOGGLE_PLAY_PAUSE, - media: currentMediaStatic!, - queuePosition: _queuePosition, + media: currentMedia, + queuePosition: currentIndex, ), ); } @@ -749,58 +659,41 @@ class Player { break; case 'SET_CURRENT_MEDIA_INDEX': - setQueuePosition = callArgs['CURRENT_MEDIA_INDEX']; - print('#QUEUE_KT PKG ${newQueue[_queuePosition].name}'); - _addUsingPlayer( + _queue.setIndex = callArgs['CURRENT_MEDIA_INDEX']; + _notifyPlayerStateChangeEvent( player, - Event( - type: EventType.SET_CURRENT_MEDIA_INDEX, - queuePosition: _queuePosition, - media: newQueue[_queuePosition], - ), + EventType.SET_CURRENT_MEDIA_INDEX, + "", ); break; case 'REPEAT_CHANGED': - final repeatMode = RepeatMode.values[callArgs['REPEAT_MODE']]; - _addUsingPlayer( + _repeatMode = RepeatMode.values[callArgs['REPEAT_MODE']]; + _notifyPlayerStateChangeEvent( player, - Event( - type: EventType.REPEAT_CHANGED, - queuePosition: _queuePosition, - media: newQueue[_queuePosition], - repeatMode: repeatMode, - ), + EventType.REPEAT_CHANGED, + "", ); break; case 'SHUFFLE_CHANGED': - final shuffleEnabled = callArgs['SHUFFLE_MODE']; - _addUsingPlayer( + _shuffleEnabled = callArgs['SHUFFLE_MODE']; + _notifyPlayerStateChangeEvent( player, - Event( - type: EventType.SHUFFLE_CHANGED, - queuePosition: _queuePosition, - media: newQueue[_queuePosition], - shuffleEnabled: shuffleEnabled, - ), + EventType.SHUFFLE_CHANGED, + "", ); break; case 'GET_INFO': // if (callArgs.values.contains('QUEUE_ARGS')) { + idSum = callArgs['ID_SUM']; final queue = callArgs['QUEUE_ARGS']; - final idSum = callArgs['ID_SUM']; final parsed = json.decode(queue) as List; final queueItems = parsed.map((json) => Media.fromJson(json)).toList(); - newQueue.clear(); - newQueue.addAll(queueItems); - _addUsingPlayer( + // _queue.clear(); + // _queue.addAll(queueItems); + _notifyPlayerStateChangeEvent( player, - Event( - type: EventType.UPDATE_QUEUE, - queue: queueItems, - queuePosition: 0, - idSum: idSum, - media: queueItems[_queuePosition], - ), + EventType.UPDATE_QUEUE, + "", ); break; @@ -812,13 +705,15 @@ class Player { _notifyRewind(Media media) async { final positionInMilli = await getCurrentPosition(); final durationInMilli = await getDuration(); - _add(Event( - type: EventType.REWIND, - media: media, - queuePosition: queuePosition, - position: Duration(milliseconds: positionInMilli), - duration: Duration(milliseconds: durationInMilli), - )); + _add( + Event( + type: EventType.REWIND, + media: media, + queuePosition: currentIndex, + position: Duration(milliseconds: positionInMilli), + duration: Duration(milliseconds: durationInMilli), + ), + ); } _notifyForward(Media media) async { @@ -828,7 +723,7 @@ class Player { _add(Event( type: EventType.FORWARD, media: media, - queuePosition: queuePosition, + queuePosition: currentIndex, position: Duration(milliseconds: positionInMilli), duration: Duration(milliseconds: durationInMilli), )); @@ -836,28 +731,24 @@ class Player { _notifyPlayerStatusChangeEvent(EventType type) { if (currentMedia != null) { - _add(Event( - type: type, media: currentMedia!, queuePosition: queuePosition)); + _add( + Event( + type: type, + media: currentMedia!, + queuePosition: currentIndex, + ), + ); } } - _notifyBeforePlayEvent(Function(bool) operation, {Media? media}) { - _add( - BeforePlayEvent( - media: media ?? currentMedia!, - queuePosition: queuePosition, - operation: operation, - ), - ); - } - static _notifyDurationChangeEvent(Player player, Duration newDuration) { - if (currentMediaStatic != null) { + final currentIndex = _queue.index; + if (_queue.current != null) { _addUsingPlayer( player, DurationChangeEvent( - media: currentMediaStatic!, - queuePosition: _queuePosition, + media: _queue.current!, + queuePosition: currentIndex, duration: newDuration)); } } @@ -867,6 +758,7 @@ class Player { EventType eventType, String error, ) { + final currentIndex = _queue.index; if (error.isNotEmpty) { _notifyPlayerErrorEvent( player: player, @@ -874,13 +766,13 @@ class Player { errorType: PlayerErrorType.INFORMATION, ); } - if (currentMediaStatic != null) { + if (_queue.current != null) { _addUsingPlayer( player, Event( type: eventType, - media: currentMediaStatic!, - queuePosition: _queuePosition, + media: _queue.current!, + queuePosition: currentIndex, ), ); } @@ -891,13 +783,14 @@ class Player { required String error, PlayerErrorType? errorType, }) { - if (currentMediaStatic != null) { + final currentIndex = _queue.index; + if (_queue.current != null) { _addUsingPlayer( player, Event( type: EventType.ERROR_OCCURED, - media: currentMediaStatic!, - queuePosition: _queuePosition, + media: _queue.current!, + queuePosition: currentIndex, error: error, errorType: errorType ?? PlayerErrorType.UNDEFINED, ), @@ -907,14 +800,15 @@ class Player { static _notifyPositionChangeEvent( Player player, Duration newPosition, Duration newDuration) { - final media = currentMediaStatic; + final media = _queue.current; + final currentIndex = _queue.index; if (media != null) { final position = newPosition.inSeconds; _addUsingPlayer( player, PositionChangeEvent( media: media, - queuePosition: _queuePosition, + queuePosition: currentIndex, position: newPosition, duration: newDuration, ), diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index e257a6bf..af2a70f6 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -5,11 +5,16 @@ import 'package:smplayer/src/isar_service.dart'; import 'package:smplayer/src/media.dart'; import 'package:smplayer/src/previous_playlist_model.dart'; import 'package:smplayer/src/queue_item.dart'; +import 'package:smplayer/src/repeat_mode.dart'; +import 'package:smplayer/src/shuffler.dart'; +import 'package:smplayer/src/simple_shuffle.dart'; class Queue { Queue({ + shuffler, + mode, this.initializeIsar = false, - }) { + }) : _shuffler = shuffler ?? SimpleShuffler() { IsarService.instance.isarEnabled = initializeIsar; itemsReady = !initializeIsar; _initialize(); @@ -30,12 +35,29 @@ class Queue { } } + var _index = 0; + int get index => _index; + Media? _current; + + set setIndex(int index) { + if (storage.isNotEmpty && index >= 0 && index <= storage.length - 1) { + _index = index; + _current = storage[index].item; + } + } + + final Shuffler _shuffler; final bool initializeIsar; bool itemsReady = false; int previousIndex = 0; PreviousPlaylistPosition? previousPosition; var storage = >[]; PreviousPlaylistMusics? previousPlaylistMusics; + DateTime? _lastPrevious; + + Media? get current => + _current ?? + (index >= 0 && index < storage.length ? storage[index].item : null); List get items { return storage.length > 0 @@ -63,15 +85,65 @@ class Queue { return previousPlaylistCurrentIndex?.currentIndex ?? 0; } + int get size => storage.length; + + Media? get top { + if (this.size > 0) { + return storage[0].item; + } + return null; + } + + play(Media media) { + if (storage.length > 0) { + storage.replaceRange(0, 1, [QueueItem(0, 0, media)]); + } else { + int pos = _nextPosition(); + storage.add(QueueItem(pos, pos, media)); + } + _save(medias: [media]); + setIndex = 0; + } + + void replaceCurrent(Media media) { + if (storage.isNotEmpty && index > -1 && index <= (storage.length - 1)) { + storage[index] = storage[index].copyWith(item: media); + } + } + + add(Media media) async { + int pos = _nextPosition(); + storage.add(QueueItem(pos, pos, media)); + await _save(medias: [media]); + } + + List> _toQueueItems(List items, int i) { + return items.map( + (e) { + i++; + return QueueItem(i, i, e); + }, + ).toList(); + } + addAll( List items, { bool shouldRemoveFirst = false, bool saveOnTop = false, }) async { - await save(medias: items, saveOnTop: saveOnTop); + final medias = shouldRemoveFirst ? items.sublist(1) : items; + + int i = storage.length == 1 ? 0 : storage.length - 1; + if (saveOnTop) { + storage.insertAll(0, _toQueueItems(medias, i)); + } else { + storage.addAll(_toQueueItems(medias, i)); + } + + await _save(medias: items, saveOnTop: saveOnTop); } - Future save( + Future _save( {required List medias, bool saveOnTop = false}) async { final items = await previousItems; debugPrint( @@ -96,7 +168,240 @@ class Queue { ...bottomList.toListStringCompressed ]; } - // } + + int removeByPosition( + {required List positionsToDelete, required bool isShuffle}) { + try { + int lastLength = storage.length; + for (var i = 0; i < positionsToDelete.length; ++i) { + final pos = positionsToDelete[i] - i; + if (pos < index) { + setIndex = index - 1; + } + storage.removeAt(pos); + } + + for (var j = 0; j < storage.length; ++j) { + storage[j].position = j; + } + + if (kDebugMode) { + for (var e in storage) { + debugPrint( + '=====> storage remove: ${e.item.name} - ${e.position} | ${e.originalPosition}'); + } + } + return lastLength - storage.length; + } catch (e) { + return 0; + } + } + + clear() => removeAll(); + + removeAll() { + storage.clear(); + setIndex = 0; + } + + shuffle() { + if (storage.length > 2) { + var current = storage[index]; + _shuffler.shuffle(storage); + for (var i = 0; i < storage.length; ++i) { + storage[i].position = i; + } + var currentIndex = storage.indexOf(current); + reorder(currentIndex, 0, true); + setIndex = 0; + } + } + + unshuffle() { + if (storage.length > 2) { + var current = storage[index]; + _shuffler.unshuffle(storage); + for (var i = 0; i < storage.length; ++i) { + final item = storage[i]; + item.position = i; + } + if (kDebugMode) { + for (var e in storage) { + debugPrint( + '=====> storage unshuffle: ${e.item.name} - ${e.position} | ${e.originalPosition}'); + } + } + setIndex = current.position; + } + } + + _nextPosition() { + if (storage.length == 0) return 0; + return storage.length; + } + + Media rewind() { + assert(index >= 0 && index < storage.length); + return storage[index].item; + } + + Media previous() { + assert(index >= 0); + final now = DateTime.now(); + if (_lastPrevious == null) { + _lastPrevious = now; + return rewind(); + } else { + final diff = now.difference(_lastPrevious!).inMilliseconds; + print("diff: $diff"); + if (diff < 3000) { + if (index > 0) { + setIndex = index - 1; + } + return storage[index].item; + } else { + _lastPrevious = now; + return rewind(); + } + } + } + + Media? possiblePrevious() { + if (index >= 0) { + final now = DateTime.now(); + if (_lastPrevious == null) { + return storage[index].item; + } else { + final diff = now.difference(_lastPrevious!).inMilliseconds; + if (diff < 3000) { + var workIndex = index; + if (index > 0) { + --workIndex; + } + return storage[workIndex].item; + } else { + return storage[index].item; + } + } + } + return storage.isNotEmpty && index >= 0 ? storage[index].item : null; + } + + Media? next() { + if (storage.length == 0) { + throw AssertionError("Queue is empty"); + } else if (storage.length > 0 && index < storage.length - 1) { + final newIndex = index + 1; + setIndex = newIndex; + var media = storage[newIndex].item; + _updateIndex(media.id, newIndex); + return media; + } else { + return null; + } + } + + Media? possibleNext(RepeatMode repeatMode) { + if (repeatMode == RepeatMode.REPEAT_MODE_OFF || + repeatMode == RepeatMode.REPEAT_MODE_ONE) { + return _next(); + } else if (repeatMode == RepeatMode.REPEAT_MODE_ALL) { + if (storage.length - 1 == index) { + return storage[0].item; + } else { + return _next(); + } + } else { + return null; + } + } + + Media? _next() { + if (storage.length == 0) { + return null; + } else if (storage.length > 0 && index < storage.length - 1) { + var media = storage[index + 1].item; + return media; + } else { + return null; + } + } + + Media? move(int pos) { + if (storage.length == 0) { + throw AssertionError("Queue is empty"); + } else if (storage.length > 0 && pos <= storage.length - 1) { + var media = storage[pos].item; + setIndex = pos; + return media; + } else { + return null; + } + } + + void _updateIndex(int id, int newIndex) async { + IsarService.instance.addPreviousPlaylistCurrentIndex( + PreviousPlaylistCurrentIndex(mediaId: id, currentIndex: newIndex), + ); + } + + Media? item(int pos) { + final item = storage[pos].item; + _updateIndex(item.id, pos); + if (storage.length == 0) { + return null; + } else if (storage.length > 0 && pos <= storage.length - 1) { + return item; + } else { + return null; + } + } + + Media restart() { + setIndex = 0; + return storage.first.item; + } + + reorder(int oldIndex, int newIndex, [bool isShuffle = false]) { + final playingItem = storage.elementAt(index); + if (newIndex > oldIndex) { + for (int i = oldIndex + 1; i <= newIndex; i++) { + if (!isShuffle) { + storage[i].originalPosition--; + } + storage[i].position--; + } + } else { + for (int i = newIndex; i < oldIndex; i++) { + if (!isShuffle) { + storage[i].originalPosition++; + } + storage[i].position++; + } + } + + storage[oldIndex].position = newIndex; + if (!isShuffle) { + storage[oldIndex].originalPosition = newIndex; + } + storage.sort((a, b) => a.position.compareTo(b.position)); + final playingIndex = storage.indexOf(playingItem); + + if (kDebugMode) { + debugPrint( + '=====> ${storage[oldIndex].item.name} - storage[oldIndex]: ${storage[oldIndex].originalPosition}', + ); + debugPrint( + '=====> ${storage[newIndex].item.name} - storage[newIndex]: ${storage[newIndex].originalPosition}', + ); + for (var e in storage) { + debugPrint( + '=====> storage Reorder: ${e.item.name} - ${e.position} - ${e.originalPosition}', + ); + } + } + setIndex = playingIndex; + } void dispose() { IsarService.instance.dispose(); From e841979d5d619734de266ba9ca4c0b8aa6510b2e Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 10 Sep 2024 09:11:09 -0300 Subject: [PATCH 27/70] fix isar and shuffle --- .../player/MediaButtonEventHandler.kt | 14 +++++-- .../br/com/suamusica/player/MediaService.kt | 42 ++++++++++++------- .../player/MediaSessionConnection.kt | 7 +++- .../suamusica/player/MethodChannelManager.kt | 4 +- .../player/MethodChannelManagerArgsBuilder.kt | 6 +-- .../suamusica/player/PlayerChangeNotifier.kt | 6 +-- .../br/com/suamusica/player/PlayerPlugin.kt | 3 +- packages/player/lib/src/player.dart | 16 +++---- 8 files changed, 53 insertions(+), 45 deletions(-) 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 68ff37ca..53761bd7 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 @@ -27,6 +27,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.FAVORITE import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST import br.com.suamusica.player.PlayerPlugin.Companion.POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL @@ -138,7 +139,12 @@ class MediaButtonEventHandler( if (customCommand.customAction == REORDER) { val oldIndex = args.getInt("oldIndex") val newIndex = args.getInt("newIndex") - mediaService.reorder(oldIndex, newIndex) + val json = args.getString(POSITIONS_LIST) + val gson = GsonBuilder().create() + val mediaListType = object : TypeToken>?>() {}.type + val positionsList: List> = gson.fromJson(json, mediaListType) + + mediaService.reorder(oldIndex, newIndex,positionsList) } if (customCommand.customAction == "onTogglePlayPause") { mediaService.togglePlayPause() @@ -164,14 +170,14 @@ class MediaButtonEventHandler( val shouldPrepare = args.getBoolean("shouldPrepare") mediaService.play(shouldPrepare) } - if (customCommand.customAction == "playFromQueue") { + if (customCommand.customAction == PLAY_FROM_QUEUE_METHOD) { mediaService.playFromQueue(args.getInt(POSITION_ARGUMENT)) } if (customCommand.customAction == "notification_previous" || customCommand.customAction == "previous") { - session.player.seekToPrevious() + session.player.seekToPreviousMediaItem() } if (customCommand.customAction == "notification_next" || customCommand.customAction == "next") { - session.player.seekToNext() + session.player.seekToNextMediaItem() } if (customCommand.customAction == "pause") { mediaService.pause() 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 23b4eb29..62e3d140 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 @@ -84,9 +84,7 @@ class MediaService : MediaSessionService() { private var shuffleOrder: DefaultShuffleOrder? = null private val artCache = HashMap() - var shuffledIndices = mutableListOf() - - + private var shuffledIndices = mutableListOf() override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) @@ -216,8 +214,8 @@ class MediaService : MediaSessionService() { player?.shuffleModeEnabled?.let { if (it) { shuffledIndices.clear() - for (element in positionsList) { - shuffledIndices.add(element["originalPosition"] ?: 0) + for (e in positionsList) { + shuffledIndices.add(e["originalPosition"] ?: 0) } shuffleOrder = DefaultShuffleOrder( shuffledIndices.toIntArray(), @@ -230,7 +228,7 @@ class MediaService : MediaSessionService() { } fun enqueue(cookie: String, medias: List, autoPlay: Boolean) { - var idSum = 0 + var idSum = 0L // if (medias.size == 1 && (player?.mediaItemCount ?: 0) > 0) { // if (player?.shuffleModeEnabled == true) { // shuffleOrder?.let { @@ -274,8 +272,9 @@ class MediaService : MediaSessionService() { player?.addMediaSource(mediaSource) } player?.prepare() + PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(idSum) } - PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(listOf(medias[0]), idSum) + PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(idSum) } } @@ -319,14 +318,24 @@ class MediaService : MediaSessionService() { } } - fun reorder(oldIndex: Int, newIndex: Int) { - player?.moveMediaItem(oldIndex, newIndex) - PlayerSingleton.playerChangeNotifier?.currentMediaIndex( - currentIndex() - ) + fun reorder( + oldIndex: Int, + newIndex: Int, + positionsList: List> + ) { + if (player?.shuffleModeEnabled == true) { + val list = shuffledIndices.ifEmpty { + positionsList.map { it["originalPosition"] ?: 0 }.toMutableList() + } + Collections.swap(list, oldIndex, newIndex) + shuffleOrder = + DefaultShuffleOrder(list.toIntArray(), System.currentTimeMillis()) + player?.setShuffleOrder(shuffleOrder!!) + } else { + player?.moveMediaItem(oldIndex, newIndex) + } } - fun removeIn(indexes: List) { if (indexes.isNotEmpty()) { indexes.forEach { @@ -490,12 +499,13 @@ class MediaService : MediaSessionService() { } fun currentIndex(): Int { - val position = if (player?.shuffleModeEnabled == true) + val position = if (player?.shuffleModeEnabled == true) { shuffledIndices.indexOf( player?.currentMediaItemIndex ?: 0 ) - else player?.currentMediaItemIndex ?: 0 -// return player?.currentMediaItemIndex ?: 0 + } else { + player?.currentMediaItemIndex ?: 0 + } return position } 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 36238062..479eace5 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 @@ -15,6 +15,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_ONE import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN @@ -68,7 +69,7 @@ class MediaSessionConnection( fun playFromQueue(index: Int) { val bundle = Bundle() bundle.putInt("position", index) - sendCommand("playFromQueue", bundle) + sendCommand(PLAY_FROM_QUEUE_METHOD, bundle) } fun play(shouldPrepare: Boolean = false) { @@ -77,10 +78,12 @@ class MediaSessionConnection( sendCommand("play", bundle) } - fun reorder(oldIndex: Int, newIndex: Int) { + fun reorder(oldIndex: Int, newIndex: Int, positionsList: List>) { val bundle = Bundle() bundle.putInt("oldIndex", oldIndex) bundle.putInt("newIndex", newIndex) + val json = Gson().toJson(positionsList) + bundle.putString(POSITIONS_LIST, json) sendCommand(REORDER, bundle) } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt index 6b8a7d0a..25982e6d 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt @@ -45,14 +45,12 @@ class MethodChannelManager(private val channel: MethodChannel) { } fun sendCurrentQueue( - queue: List, - idSum: Int, + idSum: Long, playerId: String, ) { val args = MethodChannelManagerArgsBuilder() .event("CURRENT_QUEUE") .playerId(playerId) - .queue(queue) .idSum(idSum) .build() invokeMethod("GET_INFO", args) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt index c1164cf0..cdda53c1 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt @@ -32,11 +32,7 @@ class MethodChannelManagerArgsBuilder { return this } - fun queue(queue: List): MethodChannelManagerArgsBuilder { - args["QUEUE_ARGS"] = Gson().toJson(queue) - return this - } - fun idSum(idSum: Int): MethodChannelManagerArgsBuilder { + fun idSum(idSum: Long): MethodChannelManagerArgsBuilder { args["ID_SUM"] = idSum return this } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index 25250a15..1448e2c0 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -35,11 +35,11 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { Log.i("Player", "notifyItemTransition") channelManager.notifyItemTransition("sua-musica-player") } - fun sendCurrentQueue(queue:List, idSum:Int) { + fun sendCurrentQueue(idSum: Long) { Log.i("Player", "Notifying Player Previous") - channelManager.sendCurrentQueue(queue,idSum,"sua-musica-player") + channelManager.sendCurrentQueue(idSum, "sua-musica-player") } - fun currentMediaIndex(currentMediaIndex:Int) { + fun currentMediaIndex(currentMediaIndex: Int) { Log.i("Player", "Notifying Player Previous") channelManager.currentMediaIndex("sua-musica-player", currentMediaIndex) } 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 aee86002..f3fb3790 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 @@ -147,7 +147,8 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { REORDER -> { val from = call.argument("oldIndex")!! val to = call.argument("newIndex")!! - PlayerSingleton.mediaSessionConnection?.reorder(from, to) + val positionsList = call.argument>>(POSITIONS_LIST) ?: emptyList() + PlayerSingleton.mediaSessionConnection?.reorder(from, to,positionsList) } REMOVE_ALL -> { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 59b95de7..30c90222 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:smaws/aws.dart'; import 'package:flutter/services.dart'; @@ -46,11 +45,12 @@ class Player { static late Queue _queue; static RepeatMode _repeatMode = RepeatMode.REPEAT_MODE_OFF; static bool _shuffleEnabled = false; - static int idSum = 0; + static int _idSum = 0; final mutex = Mutex(); final String playerId; + int get idSum => _idSum; bool get isShuffleEnabled => _shuffleEnabled; RepeatMode get repeatMode => _repeatMode; @@ -203,7 +203,6 @@ class Player { Future restartQueue() async { final media = _queue.restart(); - return media; } @@ -211,12 +210,12 @@ class Player { int oldIndex, int newIndex, ) async { - // _queue.reorder(oldIndex, newIndex, isShuffle); - final result = _queue.reorder(oldIndex, newIndex, isShuffleEnabled); + _queue.reorder(oldIndex, newIndex, isShuffleEnabled); debugPrint('#_queue.reorder: ${getPositionsList()}'); _channel.invokeMethod('reorder', { 'oldIndex': oldIndex, 'newIndex': newIndex, + 'positionsList': getPositionsList(), }); return Ok; } @@ -684,12 +683,7 @@ class Player { break; case 'GET_INFO': // if (callArgs.values.contains('QUEUE_ARGS')) { - idSum = callArgs['ID_SUM']; - final queue = callArgs['QUEUE_ARGS']; - final parsed = json.decode(queue) as List; - final queueItems = parsed.map((json) => Media.fromJson(json)).toList(); - // _queue.clear(); - // _queue.addAll(queueItems); + _idSum = callArgs['ID_SUM']; _notifyPlayerStateChangeEvent( player, EventType.UPDATE_QUEUE, From 663c911e571d54c372418b441cc13fcf23448ff0 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 10 Sep 2024 14:46:20 -0300 Subject: [PATCH 28/70] fix enqueue --- .../br/com/suamusica/player/MediaService.kt | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) 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 62e3d140..bdba0de6 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 @@ -229,25 +229,6 @@ class MediaService : MediaSessionService() { fun enqueue(cookie: String, medias: List, autoPlay: Boolean) { var idSum = 0L -// if (medias.size == 1 && (player?.mediaItemCount ?: 0) > 0) { -// if (player?.shuffleModeEnabled == true) { -// shuffleOrder?.let { -// it.cloneAndInsert(shuffleOrder!!.lastIndex, 1) -// player?.setShuffleOrder(it) -// for (i in 0 until it.length) { -// Log.i( -// TAG, -// "addMediaSource_one: enqueue: ${player?.getMediaItemAt(i)?.mediaMetadata?.title}" -// ) -// } -// } -// player?.setShuffleOrder(shuffleOrder!!) -// } -// player?.addMediaSource(prepare(cookie, medias[0])) -// idSum += medias[0].id -// PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(medias, idSum) -// return -// } if (medias.isNotEmpty()) { // Prepare the first media source outside the coroutine @@ -258,17 +239,19 @@ class MediaService : MediaSessionService() { player?.prepare() if (autoPlay) { player?.playWhenReady = true + PlayerSingleton.playerChangeNotifier?.notifyItemTransition() } } -// mediaButtonEventHandler.buildIcons(medias[0].isFavorite ?: false) // Use coroutine to prepare and add the remaining media sources + val workList:List = if(player?.mediaItemCount == 0) medias.drop(1) else medias.toList() + CoroutineScope(Dispatchers.Main).launch { - for (i in 1 until medias.size) { + for (i in workList.indices) { val mediaSource = withContext(Dispatchers.IO) { - Log.i(TAG, "CoroutineScope: enqueue: ${medias[i].name}") - prepare(cookie, medias[i]) + Log.i(TAG, "CoroutineScope: enqueue: ${workList[i].name}") + prepare(cookie, workList[i]) } - idSum += medias[i].id + idSum += workList[i].id player?.addMediaSource(mediaSource) } player?.prepare() @@ -542,7 +525,9 @@ class MediaService : MediaSessionService() { currentIndex() ) mediaButtonEventHandler.buildIcons() - PlayerSingleton.playerChangeNotifier?.notifyItemTransition() + if(reason != Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED){ + PlayerSingleton.playerChangeNotifier?.notifyItemTransition() + } } override fun onPlaybackStateChanged(playbackState: @Player.State Int) { From 3089c71e7dd60b3d23d1d1335ab4d969c55d4bd8 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 10 Sep 2024 15:25:49 -0300 Subject: [PATCH 29/70] enqueue batch --- packages/player/lib/src/player.dart | 42 ++++++++++++++++------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 30c90222..50023a71 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:smaws/aws.dart'; import 'package:flutter/services.dart'; @@ -131,22 +132,29 @@ class Player { }) async { _cookies = await cookieSigner(); String cookie = _cookies!.toHeaders(); - _channel - .invokeMethod( - items.length > 1 ? 'enqueue' : 'enqueue_one', - items.toListJson - ..insert( - 0, - { - 'playerId': playerId, - 'cookie': cookie, - 'shallSendEvents': _shallSendEvents, - 'externalplayback': externalPlayback, - 'autoPlay': autoPlay, - }, - ), - ) - .then((result) => result ?? Future.value(Ok)); + final int batchSize = 50; + + final List> batchArgs = items + .map((media) => { + ...media.toJson(), + 'playerId': playerId, + 'cookie': cookie, + 'shallSendEvents': _shallSendEvents, + 'externalplayback': externalPlayback, + 'autoPlay': autoPlay, + }) + .toList(); + + for (int i = 0; i < batchArgs.length; i += batchSize) { + final batch = batchArgs.sublist(i, min(i + batchSize, batchArgs.length)); + await _channel + .invokeMethod( + batch.length > 1 ? 'enqueue' : 'enqueue_one', + batch, + ) + .then((result) => result ?? Future.value(Ok)); + } + ; await IsarService.instance.removeAllMusics(); _queue.addAll(items); @@ -682,14 +690,12 @@ class Player { ); break; case 'GET_INFO': - // if (callArgs.values.contains('QUEUE_ARGS')) { _idSum = callArgs['ID_SUM']; _notifyPlayerStateChangeEvent( player, EventType.UPDATE_QUEUE, "", ); - break; default: _log('Unknown method ${call.method} '); From 7bb2ebc0696f55445a6d4c137287a8b444000e5c Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 11 Sep 2024 11:08:13 -0300 Subject: [PATCH 30/70] working before queue process --- .../player/MediaButtonEventHandler.kt | 2 + .../br/com/suamusica/player/MediaService.kt | 81 ++++++++++++------- .../player/MediaSessionConnection.kt | 3 +- .../br/com/suamusica/player/PlayerPlugin.kt | 17 ++-- packages/player/lib/src/player.dart | 24 +++--- 5 files changed, 83 insertions(+), 44 deletions(-) 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 53761bd7..78c0d6a6 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 @@ -206,6 +206,7 @@ class MediaButtonEventHandler( } if (customCommand.customAction == ENQUEUE || customCommand.customAction == ENQUEUE_ONE) { val json = args.getString("json") + val isLastBatch = args.getBoolean("isLastBatch") // Log the received JSON Log.d("Player", "First media Received JSON for enqueue: $json") val gson = GsonBuilder().create() @@ -220,6 +221,7 @@ class MediaButtonEventHandler( args.getString("cookie")!!, mediaList, args.getBoolean("autoPlay"), + isLastBatch, ) } return Futures.immediateFuture( 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 bdba0de6..9d30cceb 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 @@ -44,6 +44,7 @@ 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.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.ListenableFuture @@ -85,7 +86,7 @@ class MediaService : MediaSessionService() { private val artCache = HashMap() private var shuffledIndices = mutableListOf() - + val allMedias: MutableList = mutableListOf() override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) return Service.START_STICKY @@ -227,12 +228,22 @@ class MediaService : MediaSessionService() { } } - fun enqueue(cookie: String, medias: List, autoPlay: Boolean) { - var idSum = 0L + fun enqueue(cookie: String, medias: List, autoPlay: Boolean, isLastBatch: Boolean) { + allMedias.addAll(medias) + Log.i(TAG, "##enqueueAll: enqueue: ${allMedias.size}| $isLastBatch | autoPlay: $autoPlay") + if (isLastBatch) { + createMediaSource(cookie, allMedias, autoPlay) + allMedias.clear() + } + } + private fun createMediaSource(cookie: String, medias: List, autoPlay: Boolean) { + var idSum = 0L + val mediaSources: MutableList = mutableListOf() if (medias.isNotEmpty()) { // Prepare the first media source outside the coroutine if (player?.mediaItemCount == 0) { + val firstMediaSource = prepare(cookie, medias[0]) idSum += medias[0].id player?.setMediaSource(firstMediaSource) @@ -243,32 +254,35 @@ class MediaService : MediaSessionService() { } } // Use coroutine to prepare and add the remaining media sources - val workList:List = if(player?.mediaItemCount == 0) medias.drop(1) else medias.toList() - - CoroutineScope(Dispatchers.Main).launch { - for (i in workList.indices) { - val mediaSource = withContext(Dispatchers.IO) { + val workList: List = + if (player?.mediaItemCount == 0) medias.drop(1) else medias.toList() + val job = CoroutineScope(Dispatchers.Main).launch { + for (i in workList.indices) { + mediaSources.add(withContext(Dispatchers.IO) { Log.i(TAG, "CoroutineScope: enqueue: ${workList[i].name}") prepare(cookie, workList[i]) - } + }) idSum += workList[i].id - player?.addMediaSource(mediaSource) +// PlayerSingleton.playerChangeNotifier?.notifyItemTransition() } + player?.addMediaSources(mediaSources) + } + job.invokeOnCompletion { player?.prepare() - PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(idSum) + PlayerSingleton.playerChangeNotifier?.notifyItemTransition() + Log.i(TAG, "CoroutineScope: enqueue job completation") } - PlayerSingleton.playerChangeNotifier?.sendCurrentQueue(idSum) } } - private fun prepare(cookie: String, media: Media): MediaSource { + private fun prepare(cookie: String, media: Media, coverBytes: ByteArray? = null): MediaSource { val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setReadTimeoutMs(15 * 1000) dataSourceFactory.setConnectTimeoutMs(10 * 1000) dataSourceFactory.setUserAgent(userAgent) dataSourceFactory.setAllowCrossProtocolRedirects(true) dataSourceFactory.setDefaultRequestProperties(mapOf("Cookie" to cookie)) - val metadata = buildMetaData(media) + val metadata = buildMetaData(media, coverBytes) val url = media.url val uri = if (url.startsWith("/")) Uri.fromFile(File(url)) else Uri.parse(url) val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata) @@ -351,26 +365,18 @@ class MediaService : MediaSessionService() { } } - private fun buildMetaData(media: Media): MediaMetadata { + private fun buildMetaData(media: Media, coverBytes: ByteArray?): MediaMetadata { val metadataBuilder = MediaMetadata.Builder() - val stream = ByteArrayOutputStream() - val art = artCache[media.bigCoverUrl] ?: try { - dataSourceBitmapLoader.loadBitmap(Uri.parse(media.bigCoverUrl)) - .get(5000, TimeUnit.MILLISECONDS).also { - artCache[media.bigCoverUrl] = it - } - } catch (e: Exception) { - BitmapFactory.decodeResource(resources, R.drawable.default_art) - } - - art?.compress(Bitmap.CompressFormat.PNG, 95, stream) val bundle = Bundle() - bundle.putBoolean(PlayerPlugin.IS_FAVORITE_ARGUMENT, media.isFavorite ?: false) + bundle.putBoolean(IS_FAVORITE_ARGUMENT, media.isFavorite ?: false) metadataBuilder.apply { setAlbumTitle(media.name) setArtist(media.author) - setArtworkData(stream.toByteArray(), PICTURE_TYPE_FRONT_COVER) +// if (coverBytes != null) { +// setArtworkData(coverBytes, PICTURE_TYPE_FRONT_COVER) +// } + setArtworkUri(Uri.parse(media.bigCoverUrl)) setArtist(media.author) setTitle(media.name) setDisplayTitle(media.name) @@ -380,6 +386,23 @@ class MediaService : MediaSessionService() { return metadata } + private fun transformCoverInBytes(coverUrl: Uri): ByteArray { + val stream = ByteArrayOutputStream() + + val art = artCache[coverUrl.toString()] ?: try { + dataSourceBitmapLoader.loadBitmap(coverUrl) + .get(5000, TimeUnit.MILLISECONDS).also { + artCache[coverUrl.toString()] = it + } + } catch (e: Exception) { + BitmapFactory.decodeResource(resources, R.drawable.default_art) + } + + art?.compress(Bitmap.CompressFormat.PNG, 95, stream) + + return stream.toByteArray() + } + fun play(shouldPrepare: Boolean = false) { performAndEnableTracking { if (shouldPrepare) { @@ -525,7 +548,7 @@ class MediaService : MediaSessionService() { currentIndex() ) mediaButtonEventHandler.buildIcons() - if(reason != Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED){ + if (reason != Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) { PlayerSingleton.playerChangeNotifier?.notifyItemTransition() } } 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 479eace5..1bd8c87d 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 @@ -58,11 +58,12 @@ class MediaSessionConnection( } } - fun enqueue(cookie: String, medias: String, autoPlay: Boolean) { + fun enqueue(cookie: String, medias: String, autoPlay: Boolean, isLastBatch: Boolean) { val bundle = Bundle() bundle.putString("cookie", cookie) bundle.putString("json", medias) bundle.putBoolean("autoPlay", autoPlay) + bundle.putBoolean("isLastBatch", isLastBatch) sendCommand(ENQUEUE, bundle) } 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 f3fb3790..b7d271ad 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 @@ -129,13 +129,20 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { LOAD_METHOD -> {} ENQUEUE_ONE, ENQUEUE -> { - val listMedia: List> = call.arguments()!! - val arg = listMedia[0] - val json = Gson().toJson(listMedia.drop(1)) +// val listMedia: List> = call.arguments()!! +// val arg = listMedia[0] + val batch: Map = call.arguments()!! + val listMedia: List> = batch["batch"] as List> + val isLastBatch: Boolean = batch["isLastBatch"] as Boolean + + + PlayerSingleton.externalPlayback = listMedia[0]["externalplayback"]!! == "true" + val json = Gson().toJson(listMedia) PlayerSingleton.mediaSessionConnection?.enqueue( - arg["cookie"] as String, + cookie, json, - arg["autoPlay"] as Boolean + listMedia[0]["autoPlay"].toString() == "true", + isLastBatch, ) } diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 50023a71..85322fd7 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -132,7 +132,7 @@ class Player { }) async { _cookies = await cookieSigner(); String cookie = _cookies!.toHeaders(); - final int batchSize = 50; + final int batchSize = 80; final List> batchArgs = items .map((media) => { @@ -144,17 +144,23 @@ class Player { 'autoPlay': autoPlay, }) .toList(); - + int j = 0; for (int i = 0; i < batchArgs.length; i += batchSize) { final batch = batchArgs.sublist(i, min(i + batchSize, batchArgs.length)); - await _channel - .invokeMethod( - batch.length > 1 ? 'enqueue' : 'enqueue_one', - batch, - ) - .then((result) => result ?? Future.value(Ok)); + final bool isLastBatch = (items.length / batchSize).ceil() == ++j; + debugPrint( + '##enqueueAll $isLastBatch | ${(items.length / batchSize).ceil()} | $j'); + unawaited( + _channel.invokeMethod( + items.length > 1 ? 'enqueue' : 'enqueue_one', + { + 'batch': batch, + 'isLastBatch': isLastBatch, + }, + ), + ); + // .then((result) => result ?? Future.value(Ok)); } - ; await IsarService.instance.removeAllMusics(); _queue.addAll(items); From 1c2f538f19422290dade0a4c49c2e1200e8808d1 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 11 Sep 2024 14:49:28 -0300 Subject: [PATCH 31/70] added queue process --- .../br/com/suamusica/player/MediaService.kt | 77 +++++++++++-------- .../br/com/suamusica/player/PlayerPlugin.kt | 34 ++++---- packages/player/lib/src/player.dart | 13 ++-- 3 files changed, 69 insertions(+), 55 deletions(-) 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 9d30cceb..84d3234d 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 @@ -45,11 +45,15 @@ import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.cookie import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream @@ -86,7 +90,7 @@ class MediaService : MediaSessionService() { private val artCache = HashMap() private var shuffledIndices = mutableListOf() - val allMedias: MutableList = mutableListOf() + private var autoPlay: Boolean = true override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) return Service.START_STICKY @@ -228,50 +232,57 @@ class MediaService : MediaSessionService() { } } - fun enqueue(cookie: String, medias: List, autoPlay: Boolean, isLastBatch: Boolean) { - allMedias.addAll(medias) - Log.i(TAG, "##enqueueAll: enqueue: ${allMedias.size}| $isLastBatch | autoPlay: $autoPlay") - if (isLastBatch) { - createMediaSource(cookie, allMedias, autoPlay) - allMedias.clear() + private val channel = Channel>(Channel.BUFFERED) + private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + // Function to add items to the channel + private fun addToQueue(item: List) { + serviceScope.launch { + channel.send(item) } } + // Function to process items (replace with your actual processing logic) + private fun processItem(item: List) { + createMediaSource(cookie, item, autoPlay) + } + + private val consumer = serviceScope.launch { + channel.receiveAsFlow().collect { item -> + Log.i(TAG, "[TESTE QUEUE] processItem: ${item.size}") + processItem(item) + } + } + + fun enqueue( + cookie: String, + medias: List, + autoPlay: Boolean, + isLastBatch: Boolean + ) { +// this.cookie = cookie + this.autoPlay = autoPlay + Log.i(TAG, "[TESTE QUEUE] enqueue: ${medias.size}") + addToQueue(medias) + } + private fun createMediaSource(cookie: String, medias: List, autoPlay: Boolean) { var idSum = 0L val mediaSources: MutableList = mutableListOf() if (medias.isNotEmpty()) { - // Prepare the first media source outside the coroutine - if (player?.mediaItemCount == 0) { - - val firstMediaSource = prepare(cookie, medias[0]) - idSum += medias[0].id - player?.setMediaSource(firstMediaSource) - player?.prepare() - if (autoPlay) { - player?.playWhenReady = true - PlayerSingleton.playerChangeNotifier?.notifyItemTransition() - } - } - // Use coroutine to prepare and add the remaining media sources - val workList: List = - if (player?.mediaItemCount == 0) medias.drop(1) else medias.toList() - val job = CoroutineScope(Dispatchers.Main).launch { - for (i in workList.indices) { - mediaSources.add(withContext(Dispatchers.IO) { - Log.i(TAG, "CoroutineScope: enqueue: ${workList[i].name}") - prepare(cookie, workList[i]) - }) - idSum += workList[i].id -// PlayerSingleton.playerChangeNotifier?.notifyItemTransition() +// val job = CoroutineScope(Dispatchers.Main).launch { + for (i in medias.indices) { + Log.i(TAG, "[TESTE QUEUE]CoroutineScope: enqueue: ${medias[i].name}") + mediaSources.add(prepare(cookie, medias[i])) + idSum += medias[i].id } player?.addMediaSources(mediaSources) - } - job.invokeOnCompletion { +// } +// job.invokeOnCompletion { player?.prepare() PlayerSingleton.playerChangeNotifier?.notifyItemTransition() Log.i(TAG, "CoroutineScope: enqueue job completation") - } +// } } } 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 b7d271ad..ac31b2b0 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 @@ -63,6 +63,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val TAG = "Player" const val Ok = 1 private var alreadyAttachedToActivity: Boolean = false + var cookie = "" } override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { @@ -112,13 +113,13 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { } private fun handleMethodCall(call: MethodCall, response: MethodChannel.Result) { - val cookie: String? if (call.method == ENQUEUE || call.method == ENQUEUE_ONE) { - val listMedia: List> = call.arguments()!! - cookie = listMedia[0]["cookie"]!! - PlayerSingleton.externalPlayback = listMedia[0]["externalplayback"]!! == "true" + val batch: Map = call.arguments()!! + cookie = if (batch.containsKey("cookie")) batch["cookie"] as String else cookie + PlayerSingleton.externalPlayback = + if (batch.containsKey("externalplayback")) batch["externalplayback"].toString() == "true" else PlayerSingleton.externalPlayback } else { - cookie = call.argument("cookie") + cookie = call.argument("cookie") ?: "" PlayerSingleton.externalPlayback = call.argument("externalplayback") } Log.d( @@ -131,17 +132,16 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { ENQUEUE -> { // val listMedia: List> = call.arguments()!! // val arg = listMedia[0] - val batch: Map = call.arguments()!! - val listMedia: List> = batch["batch"] as List> + val batch: Map = call.arguments()!! + val listMedia: List> = + batch["batch"] as List> val isLastBatch: Boolean = batch["isLastBatch"] as Boolean - - - PlayerSingleton.externalPlayback = listMedia[0]["externalplayback"]!! == "true" + val autoPlay: Boolean = (batch["autoPlay"] ?: false) as Boolean val json = Gson().toJson(listMedia) PlayerSingleton.mediaSessionConnection?.enqueue( - cookie, + cookie, json, - listMedia[0]["autoPlay"].toString() == "true", + autoPlay, isLastBatch, ) } @@ -154,8 +154,9 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { REORDER -> { val from = call.argument("oldIndex")!! val to = call.argument("newIndex")!! - val positionsList = call.argument>>(POSITIONS_LIST) ?: emptyList() - PlayerSingleton.mediaSessionConnection?.reorder(from, to,positionsList) + val positionsList = + call.argument>>(POSITIONS_LIST) ?: emptyList() + PlayerSingleton.mediaSessionConnection?.reorder(from, to, positionsList) } REMOVE_ALL -> { @@ -172,7 +173,8 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { } TOGGLE_SHUFFLE -> { - val positionsList = call.argument>>(POSITIONS_LIST) ?: emptyList() + val positionsList = + call.argument>>(POSITIONS_LIST) ?: emptyList() PlayerSingleton.mediaSessionConnection?.toggleShuffle(positionsList) } @@ -191,7 +193,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { UPDATE_FAVORITE -> { val isFavorite = call.argument(IS_FAVORITE_ARGUMENT) ?: false val idFavorite = call.argument(ID_FAVORITE_ARGUMENT) ?: 0 - PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite,idFavorite) + PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite, idFavorite) } PLAY_FROM_QUEUE_METHOD -> { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 85322fd7..93cbf859 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -137,11 +137,6 @@ class Player { final List> batchArgs = items .map((media) => { ...media.toJson(), - 'playerId': playerId, - 'cookie': cookie, - 'shallSendEvents': _shallSendEvents, - 'externalplayback': externalPlayback, - 'autoPlay': autoPlay, }) .toList(); int j = 0; @@ -156,10 +151,16 @@ class Player { { 'batch': batch, 'isLastBatch': isLastBatch, + if (i == 0) ...{ + 'playerId': playerId, + 'cookie': cookie, + 'shallSendEvents': _shallSendEvents, + 'externalplayback': externalPlayback, + 'autoPlay': autoPlay, + }, }, ), ); - // .then((result) => result ?? Future.value(Ok)); } await IsarService.instance.removeAllMusics(); From f7fa7ed295532075dea763578663d65873f13096 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 11 Sep 2024 16:05:04 -0300 Subject: [PATCH 32/70] fix auto play --- packages/player/lib/src/player.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 93cbf859..3a0c6597 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -151,12 +151,12 @@ class Player { { 'batch': batch, 'isLastBatch': isLastBatch, + 'autoPlay': autoPlay, + 'playerId': playerId, + 'shallSendEvents': _shallSendEvents, + 'externalplayback': externalPlayback, if (i == 0) ...{ - 'playerId': playerId, 'cookie': cookie, - 'shallSendEvents': _shallSendEvents, - 'externalplayback': externalPlayback, - 'autoPlay': autoPlay, }, }, ), From 28bae18ceaf1486611d0d61d4918034a3f108119 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 11 Sep 2024 22:03:34 -0300 Subject: [PATCH 33/70] added localMediaValidator --- packages/player/lib/src/media.dart | 8 -------- packages/player/lib/src/player.dart | 20 +++++++++++++++----- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/player/lib/src/media.dart b/packages/player/lib/src/media.dart index b4bbaf09..ce6be301 100644 --- a/packages/player/lib/src/media.dart +++ b/packages/player/lib/src/media.dart @@ -196,14 +196,6 @@ extension ListMediaToListStringCompressed on List { List get toListStringCompressed => map((e) => e.toString()).toList(); List> get toListJson => map((e) => e.toJson()).toList(); - - // List get toQueueItemData => map((e) { - // return QueueItemData( - // position: e.indexInPlaylist ?? 0, - // media: e, - // markedForDelete: false, - // ); - // }).toList(); } extension ListStringToListPlayable on List { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 3a0c6597..b6cfa63d 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -62,7 +62,7 @@ class Player { StreamController(); final Future Function() cookieSigner; - final Future Function(Media)? localMediaValidator; + final String? Function(Media)? localMediaValidator; final bool autoPlay; final chromeCastEnabledEvents = [ EventType.BEFORE_PLAY, @@ -129,15 +129,25 @@ class Player { bool shouldRemoveFirst = false, bool saveOnTop = false, bool autoPlay = false, + // Future Function( + // int albumId, + // String url, + // )? getLocalCover, }) async { _cookies = await cookieSigner(); String cookie = _cookies!.toHeaders(); final int batchSize = 80; final List> batchArgs = items - .map((media) => { - ...media.toJson(), - }) + .map( + (media) => { + ...media + .copyWith( + url: localMediaValidator?.call(media) ?? media.url, + ) + .toJson(), + }, + ) .toList(); int j = 0; for (int i = 0; i < batchArgs.length; i += batchSize) { @@ -147,7 +157,7 @@ class Player { '##enqueueAll $isLastBatch | ${(items.length / batchSize).ceil()} | $j'); unawaited( _channel.invokeMethod( - items.length > 1 ? 'enqueue' : 'enqueue_one', + 'enqueue', { 'batch': batch, 'isLastBatch': isLastBatch, From f504ab4fcc18d19448da45619bb0c99bc9b2bf85 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 12 Sep 2024 10:49:55 -0300 Subject: [PATCH 34/70] fix favorite and remove unused code --- .../player/MediaButtonEventHandler.kt | 11 +----- .../br/com/suamusica/player/MediaService.kt | 30 ++++++--------- .../player/MediaSessionConnection.kt | 4 +- .../br/com/suamusica/player/PlayerPlugin.kt | 26 ------------- packages/player/example/lib/sm_player.dart | 2 +- packages/player/lib/src/event_type.dart | 1 - packages/player/lib/src/player.dart | 38 +++++++------------ 7 files changed, 28 insertions(+), 84 deletions(-) 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 78c0d6a6..f6840a08 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 @@ -197,7 +197,7 @@ class MediaButtonEventHandler( } } } - PlayerSingleton.favorite(true) + PlayerSingleton.favorite(isFavorite) // } } if (customCommand.customAction == "ads_playing" || customCommand.customAction == "remove_notification") { @@ -206,22 +206,13 @@ class MediaButtonEventHandler( } if (customCommand.customAction == ENQUEUE || customCommand.customAction == ENQUEUE_ONE) { val json = args.getString("json") - val isLastBatch = args.getBoolean("isLastBatch") - // Log the received JSON - Log.d("Player", "First media Received JSON for enqueue: $json") val gson = GsonBuilder().create() val mediaListType = object : TypeToken>() {}.type val mediaList: List = gson.fromJson(json, mediaListType) - // Log the first item for debugging - if (mediaList.isNotEmpty()) { - Log.d("Player", "First media item: ${gson.toJson(mediaList.first())}") - } buildIcons() mediaService.enqueue( - args.getString("cookie")!!, mediaList, args.getBoolean("autoPlay"), - isLastBatch, ) } return Futures.immediateFuture( 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 84d3234d..6c186904 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 @@ -52,6 +52,7 @@ import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @@ -193,6 +194,8 @@ class MediaService : MediaSessionService() { release() mediaSession.release() } + consumer.cancel() + channel.cancel() releasePossibleLeaks() stopSelf() super.onDestroy() @@ -249,40 +252,31 @@ class MediaService : MediaSessionService() { private val consumer = serviceScope.launch { channel.receiveAsFlow().collect { item -> - Log.i(TAG, "[TESTE QUEUE] processItem: ${item.size}") processItem(item) } } fun enqueue( - cookie: String, medias: List, autoPlay: Boolean, - isLastBatch: Boolean ) { -// this.cookie = cookie this.autoPlay = autoPlay Log.i(TAG, "[TESTE QUEUE] enqueue: ${medias.size}") addToQueue(medias) } private fun createMediaSource(cookie: String, medias: List, autoPlay: Boolean) { - var idSum = 0L val mediaSources: MutableList = mutableListOf() if (medias.isNotEmpty()) { -// val job = CoroutineScope(Dispatchers.Main).launch { - for (i in medias.indices) { - Log.i(TAG, "[TESTE QUEUE]CoroutineScope: enqueue: ${medias[i].name}") - mediaSources.add(prepare(cookie, medias[i])) - idSum += medias[i].id - } - player?.addMediaSources(mediaSources) -// } -// job.invokeOnCompletion { - player?.prepare() - PlayerSingleton.playerChangeNotifier?.notifyItemTransition() - Log.i(TAG, "CoroutineScope: enqueue job completation") -// } + for (i in medias.indices) { + Log.i(TAG, "[TESTE QUEUE]CoroutineScope: enqueue: ${medias[i].name}") + mediaSources.add(prepare(cookie, medias[i])) + } + player?.addMediaSources(mediaSources) + + player?.prepare() + + Log.i(TAG, "CoroutineScope: enqueue job completation") } } 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 1bd8c87d..b7dca05a 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 @@ -58,12 +58,10 @@ class MediaSessionConnection( } } - fun enqueue(cookie: String, medias: String, autoPlay: Boolean, isLastBatch: Boolean) { + fun enqueue( medias: String, autoPlay: Boolean) { val bundle = Bundle() - bundle.putString("cookie", cookie) bundle.putString("json", medias) bundle.putBoolean("autoPlay", autoPlay) - bundle.putBoolean("isLastBatch", isLastBatch) sendCommand(ENQUEUE, bundle) } 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 ac31b2b0..187e9df2 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 @@ -130,19 +130,14 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { LOAD_METHOD -> {} ENQUEUE_ONE, ENQUEUE -> { -// val listMedia: List> = call.arguments()!! -// val arg = listMedia[0] val batch: Map = call.arguments()!! val listMedia: List> = batch["batch"] as List> - val isLastBatch: Boolean = batch["isLastBatch"] as Boolean val autoPlay: Boolean = (batch["autoPlay"] ?: false) as Boolean val json = Gson().toJson(listMedia) PlayerSingleton.mediaSessionConnection?.enqueue( - cookie, json, autoPlay, - isLastBatch, ) } @@ -197,27 +192,6 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { } PLAY_FROM_QUEUE_METHOD -> { -// val name = call.argument(NAME_ARGUMENT)!! -// val author = call.argument(AUTHOR_ARGUMENT)!! -// val url = call.argument(URL_ARGUMENT)!! -// val coverUrl = call.argument(COVER_URL_ARGUMENT)!! -// val bigCoverUrl = call.argument(BIG_COVER_URL_ARGUMENT) -// val position = call.argument(POSITION_ARGUMENT) -// val loadOnly = call.argument(LOAD_ONLY)!! -// val isFavorite: Boolean? = call.argument(IS_FAVORITE_ARGUMENT) -// -// PlayerSingleton.mediaSessionConnection?.prepare( -// cookie!!, -// Media(name, author, url, coverUrl, bigCoverUrl, isFavorite) -// ) -// Log.d(TAG, "before prepare: cookie: $cookie") -// position?.let { -// PlayerSingleton.mediaSessionConnection?.seek(it.toLong(), true) -// } -// -// if (!loadOnly) { -// PlayerSingleton.mediaSessionConnection?.play() -// } val position = call.argument(POSITION_ARGUMENT) ?: 0 PlayerSingleton.mediaSessionConnection?.playFromQueue( position diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index d1cc7345..8da8ccd4 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -36,7 +36,7 @@ class _SMPlayerState extends State { playerId: "smplayer", cookieSigner: cookieSigner, autoPlay: false, - localMediaValidator: (m) async => m.url, + localMediaValidator: (m) => m.url, initializeIsar: false, ); player.onEvent.listen((Event event) async { diff --git a/packages/player/lib/src/event_type.dart b/packages/player/lib/src/event_type.dart index a407f59a..8a715580 100644 --- a/packages/player/lib/src/event_type.dart +++ b/packages/player/lib/src/event_type.dart @@ -2,7 +2,6 @@ enum EventType { PLAY_REQUESTED, PLAY_NOTIFICATION, BEFORE_PLAY, - UPDATE_QUEUE, SET_CURRENT_MEDIA_INDEX, BUFFERING, PLAYING, diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index b6cfa63d..7bb8cb94 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -137,30 +137,26 @@ class Player { _cookies = await cookieSigner(); String cookie = _cookies!.toHeaders(); final int batchSize = 80; - - final List> batchArgs = items - .map( - (media) => { - ...media - .copyWith( - url: localMediaValidator?.call(media) ?? media.url, - ) - .toJson(), - }, - ) - .toList(); - int j = 0; + _idSum = 0; + final List> batchArgs = items.map( + (media) { + _idSum += media.id; + return { + ...media + .copyWith( + url: localMediaValidator?.call(media) ?? media.url, + ) + .toJson(), + }; + }, + ).toList(); for (int i = 0; i < batchArgs.length; i += batchSize) { final batch = batchArgs.sublist(i, min(i + batchSize, batchArgs.length)); - final bool isLastBatch = (items.length / batchSize).ceil() == ++j; - debugPrint( - '##enqueueAll $isLastBatch | ${(items.length / batchSize).ceil()} | $j'); unawaited( _channel.invokeMethod( 'enqueue', { 'batch': batch, - 'isLastBatch': isLastBatch, 'autoPlay': autoPlay, 'playerId': playerId, 'shallSendEvents': _shallSendEvents, @@ -706,14 +702,6 @@ class Player { "", ); break; - case 'GET_INFO': - _idSum = callArgs['ID_SUM']; - _notifyPlayerStateChangeEvent( - player, - EventType.UPDATE_QUEUE, - "", - ); - break; default: _log('Unknown method ${call.method} '); } From 963669576a0a0411ec1b6cb1896504086c09a905 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 13 Sep 2024 10:05:47 -0300 Subject: [PATCH 35/70] restore isar playlist --- .../player/MediaButtonEventHandler.kt | 11 +- .../br/com/suamusica/player/MediaService.kt | 122 ++++++++++-------- .../player/MediaSessionConnection.kt | 11 +- .../br/com/suamusica/player/PlayerPlugin.kt | 15 ++- packages/player/lib/src/event_type.dart | 1 + packages/player/lib/src/player.dart | 35 +++-- packages/player/lib/src/player_state.dart | 1 + packages/player/lib/src/queue.dart | 6 +- 8 files changed, 116 insertions(+), 86 deletions(-) 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 f6840a08..e4d72675 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 @@ -27,6 +27,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.FAVORITE import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST import br.com.suamusica.player.PlayerPlugin.Companion.POSITION_ARGUMENT @@ -34,6 +35,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN import br.com.suamusica.player.PlayerPlugin.Companion.REORDER import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE import com.google.common.collect.ImmutableList @@ -144,7 +146,7 @@ class MediaButtonEventHandler( val mediaListType = object : TypeToken>?>() {}.type val positionsList: List> = gson.fromJson(json, mediaListType) - mediaService.reorder(oldIndex, newIndex,positionsList) + mediaService.reorder(oldIndex, newIndex, positionsList) } if (customCommand.customAction == "onTogglePlayPause") { mediaService.togglePlayPause() @@ -171,7 +173,12 @@ class MediaButtonEventHandler( mediaService.play(shouldPrepare) } if (customCommand.customAction == PLAY_FROM_QUEUE_METHOD) { - mediaService.playFromQueue(args.getInt(POSITION_ARGUMENT)) + mediaService.playFromQueue( + args.getInt(POSITION_ARGUMENT), args.getLong(TIME_POSITION_ARGUMENT), + args.getBoolean( + LOAD_ONLY + ), + ) } if (customCommand.customAction == "notification_previous" || customCommand.customAction == "previous") { session.player.seekToPreviousMediaItem() 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 6c186904..24402983 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 @@ -24,6 +24,10 @@ import androidx.media3.common.Player.MediaItemTransitionReason import androidx.media3.common.Player.REPEAT_MODE_ALL import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Player.REPEAT_MODE_ONE +import androidx.media3.common.Player.STATE_BUFFERING +import androidx.media3.common.Player.STATE_ENDED +import androidx.media3.common.Player.STATE_IDLE +import androidx.media3.common.Player.STATE_READY import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util @@ -92,25 +96,10 @@ class MediaService : MediaSessionService() { private val artCache = HashMap() private var shuffledIndices = mutableListOf() private var autoPlay: Boolean = true - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - return Service.START_STICKY - } - private fun getPendingIntent(): PendingIntent { - val notifyIntent = Intent("SUA_MUSICA_FLUTTER_NOTIFICATION_CLICK").apply { - addCategory(Intent.CATEGORY_DEFAULT) - flags = - Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - } + private val channel = Channel>(Channel.BUFFERED) + private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - return PendingIntent.getActivity( - applicationContext, - 0, - notifyIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT else PendingIntent.FLAG_UPDATE_CURRENT - ) - } override fun onCreate() { super.onCreate() @@ -172,6 +161,26 @@ class MediaService : MediaSessionService() { } } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + return Service.START_STICKY + } + + private fun getPendingIntent(): PendingIntent { + val notifyIntent = Intent("SUA_MUSICA_FLUTTER_NOTIFICATION_CLICK").apply { + addCategory(Intent.CATEGORY_DEFAULT) + flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + } + + return PendingIntent.getActivity( + applicationContext, + 0, + notifyIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT else PendingIntent.FLAG_UPDATE_CURRENT + ) + } + override fun onGetSession( controllerInfo: MediaSession.ControllerInfo ): MediaSession = mediaSession @@ -235,17 +244,12 @@ class MediaService : MediaSessionService() { } } - private val channel = Channel>(Channel.BUFFERED) - private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - - // Function to add items to the channel private fun addToQueue(item: List) { serviceScope.launch { channel.send(item) } } - // Function to process items (replace with your actual processing logic) private fun processItem(item: List) { createMediaSource(cookie, item, autoPlay) } @@ -260,8 +264,14 @@ class MediaService : MediaSessionService() { medias: List, autoPlay: Boolean, ) { + Log.d( + TAG, + "onMediaItemTransition: mediaItemCount: ${player?.mediaItemCount} | autoPlay: $autoPlay" + ) this.autoPlay = autoPlay - Log.i(TAG, "[TESTE QUEUE] enqueue: ${medias.size}") + if (player?.mediaItemCount == 0) { + player?.playWhenReady = autoPlay + } addToQueue(medias) } @@ -269,14 +279,10 @@ class MediaService : MediaSessionService() { val mediaSources: MutableList = mutableListOf() if (medias.isNotEmpty()) { for (i in medias.indices) { - Log.i(TAG, "[TESTE QUEUE]CoroutineScope: enqueue: ${medias[i].name}") mediaSources.add(prepare(cookie, medias[i])) } player?.addMediaSources(mediaSources) - player?.prepare() - - Log.i(TAG, "CoroutineScope: enqueue job completation") } } @@ -378,9 +384,6 @@ class MediaService : MediaSessionService() { metadataBuilder.apply { setAlbumTitle(media.name) setArtist(media.author) -// if (coverBytes != null) { -// setArtworkData(coverBytes, PICTURE_TYPE_FRONT_COVER) -// } setArtworkUri(Uri.parse(media.bigCoverUrl)) setArtist(media.author) setTitle(media.name) @@ -391,23 +394,6 @@ class MediaService : MediaSessionService() { return metadata } - private fun transformCoverInBytes(coverUrl: Uri): ByteArray { - val stream = ByteArrayOutputStream() - - val art = artCache[coverUrl.toString()] ?: try { - dataSourceBitmapLoader.loadBitmap(coverUrl) - .get(5000, TimeUnit.MILLISECONDS).also { - artCache[coverUrl.toString()] = it - } - } catch (e: Exception) { - BitmapFactory.decodeResource(resources, R.drawable.default_art) - } - - art?.compress(Bitmap.CompressFormat.PNG, 95, stream) - - return stream.toByteArray() - } - fun play(shouldPrepare: Boolean = false) { performAndEnableTracking { if (shouldPrepare) { @@ -417,11 +403,16 @@ class MediaService : MediaSessionService() { } } - fun playFromQueue(position: Int) { + fun playFromQueue(position: Int, timePosition: Long, loadOnly: Boolean = false) { + player?.playWhenReady = !loadOnly + player?.seekTo( if (player?.shuffleModeEnabled == true) shuffledIndices[position] else position, - 0 + timePosition, ) + if (!loadOnly) { + player?.prepare() + } } fun removeNotification() { @@ -537,7 +528,7 @@ class MediaService : MediaSessionService() { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) if (isPlaying) { - PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PLAYING) +// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PLAYING) startTrackingProgress() } else { stopTrackingProgressAndPerformTask {} @@ -549,19 +540,40 @@ class MediaService : MediaSessionService() { reason: @MediaItemTransitionReason Int ) { super.onMediaItemTransition(mediaItem, reason) - PlayerSingleton.playerChangeNotifier?.currentMediaIndex( - currentIndex() - ) + Log.d(TAG, "onMediaItemTransition: reason: ${reason}") + if ((player?.mediaItemCount ?: 0) > 0 +// && intArrayOf( +// Player.MEDIA_ITEM_TRANSITION_REASON_AUTO, +// Player.MEDIA_ITEM_TRANSITION_REASON_SEEK +// ).contains(reason) + ) { + PlayerSingleton.playerChangeNotifier?.currentMediaIndex( + currentIndex() + ) + } mediaButtonEventHandler.buildIcons() if (reason != Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) { + player?.playWhenReady = true PlayerSingleton.playerChangeNotifier?.notifyItemTransition() } } override fun onPlaybackStateChanged(playbackState: @Player.State Int) { super.onPlaybackStateChanged(playbackState) - PlayerSingleton.playerChangeNotifier?.notifyStateChange(playbackState) - if (playbackState == Player.STATE_ENDED) { + +// if(playbackState == STATE_IDLE) { +// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_NONE) +// } else if(playbackState == STATE_BUFFERING) { +// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_BUFFERING) +// } else if(playbackState == STATE_READY) { +// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PLAYING) +// startTrackingProgress() +// } else if(playbackState == STATE_ENDED) { +// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_STOPPED) +// } +// Log.d(TAG, "#onPlaybackStateChanged: playbackState: $playbackState") +// PlayerSingleton.playerChangeNotifier?.notifyStateChange(playbackState) + if (playbackState == STATE_ENDED) { stopTrackingProgressAndPerformTask {} } } 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 b7dca05a..5be98e22 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 @@ -15,12 +15,15 @@ import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_ONE import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST +import br.com.suamusica.player.PlayerPlugin.Companion.POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN import br.com.suamusica.player.PlayerPlugin.Companion.REORDER import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE import com.google.gson.Gson @@ -65,9 +68,11 @@ class MediaSessionConnection( sendCommand(ENQUEUE, bundle) } - fun playFromQueue(index: Int) { + fun playFromQueue(index: Int, timePosition:Long, loadOnly:Boolean) { val bundle = Bundle() - bundle.putInt("position", index) + bundle.putInt(POSITION_ARGUMENT, index) + bundle.putLong(TIME_POSITION_ARGUMENT, timePosition) + bundle.putBoolean(LOAD_ONLY, loadOnly) sendCommand(PLAY_FROM_QUEUE_METHOD, bundle) } @@ -226,7 +231,7 @@ class MediaSessionConnection( if (lastState != state.state) { Log.i(TAG, "onPlaybackStateChanged2: $state") lastState = state.state -// playerChangeNotifier.notifyStateChange(state.state) + playerChangeNotifier.notifyStateChange(state.state) } } 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 187e9df2..d59d9930 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 IS_FAVORITE_ARGUMENT = "isFavorite" const val ID_FAVORITE_ARGUMENT = "idFavorite" const val POSITION_ARGUMENT = "position" + const val TIME_POSITION_ARGUMENT = "timePosition" const val INDEXES_TO_REMOVE = "indexesToRemove" const val POSITIONS_LIST = "positionsList" const val LOAD_ONLY = "loadOnly" @@ -147,11 +148,13 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { } REORDER -> { - val from = call.argument("oldIndex")!! - val to = call.argument("newIndex")!! + val from = call.argument("oldIndex") + val to = call.argument("newIndex") val positionsList = call.argument>>(POSITIONS_LIST) ?: emptyList() - PlayerSingleton.mediaSessionConnection?.reorder(from, to, positionsList) + if (from != null && to != null) { + PlayerSingleton.mediaSessionConnection?.reorder(from, to, positionsList) + } } REMOVE_ALL -> { @@ -193,8 +196,12 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { PLAY_FROM_QUEUE_METHOD -> { val position = call.argument(POSITION_ARGUMENT) ?: 0 + val timePosition = call.argument(TIME_POSITION_ARGUMENT) ?: 0 + val loadOnly = call.argument(LOAD_ONLY) ?: false PlayerSingleton.mediaSessionConnection?.playFromQueue( - position + position, + timePosition.toLong(), + loadOnly ) } diff --git a/packages/player/lib/src/event_type.dart b/packages/player/lib/src/event_type.dart index 8a715580..f1925b99 100644 --- a/packages/player/lib/src/event_type.dart +++ b/packages/player/lib/src/event_type.dart @@ -36,6 +36,7 @@ enum EventType { ITEM_TRANSITION, REPEAT_CHANGED, SHUFFLE_CHANGED, + STATE_ENDED, } enum PlayerErrorType { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 7bb8cb94..b3ad3c77 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -52,6 +52,7 @@ class Player { final String playerId; int get idSum => _idSum; + set idSum(int value) => _idSum = value; bool get isShuffleEnabled => _shuffleEnabled; RepeatMode get repeatMode => _repeatMode; @@ -109,25 +110,12 @@ class Player { .then((result) => result ?? Future.value(Ok)); } - Future enqueue({ - required Media media, - bool autoPlay = false, - }) async { - enqueueAll( - [media], - autoPlay: autoPlay, - ); - return Ok; - } - set setQueuePosition(int position) { _queue.setIndex = position; } Future enqueueAll( List items, { - bool shouldRemoveFirst = false, - bool saveOnTop = false, bool autoPlay = false, // Future Function( // int albumId, @@ -262,18 +250,17 @@ class Player { Future playFromQueue( int pos, { - double volume = 1.0, Duration? position, - bool respectSilence = false, - bool stayAwake = false, - bool shallNotify = false, bool loadOnly = false, }) async { if (!loadOnly) { _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); } - return _channel.invokeMethod( - 'playFromQueue', {'position': pos}).then((result) => result); + return _channel.invokeMethod('playFromQueue', { + 'position': pos, + 'timePosition': position?.inMilliseconds, + 'loadOnly': loadOnly, + }).then((result) => result); } Future _doPlay({ @@ -577,6 +564,14 @@ class Player { // _handleOnComplete(player); break; + case PlayerState.STATE_ENDED: + _notifyPlayerStateChangeEvent( + player, + EventType.STATE_ENDED, + error, + ); + break; + case PlayerState.ERROR: final error = callArgs['error'] ?? "Unknown from Source"; final isPermissionError = @@ -680,6 +675,8 @@ class Player { break; case 'SET_CURRENT_MEDIA_INDEX': _queue.setIndex = callArgs['CURRENT_MEDIA_INDEX']; + _queue.updateIsarIndex( + currentMedia!.id, callArgs['CURRENT_MEDIA_INDEX']); _notifyPlayerStateChangeEvent( player, EventType.SET_CURRENT_MEDIA_INDEX, diff --git a/packages/player/lib/src/player_state.dart b/packages/player/lib/src/player_state.dart index 96896623..907bd697 100644 --- a/packages/player/lib/src/player_state.dart +++ b/packages/player/lib/src/player_state.dart @@ -9,4 +9,5 @@ enum PlayerState { SEEK_END, BUFFER_EMPTY, ITEM_TRANSITION, + STATE_ENDED, } diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index af2a70f6..5f261896 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -294,7 +294,7 @@ class Queue { final newIndex = index + 1; setIndex = newIndex; var media = storage[newIndex].item; - _updateIndex(media.id, newIndex); + updateIsarIndex(media.id, newIndex); return media; } else { return null; @@ -339,7 +339,7 @@ class Queue { } } - void _updateIndex(int id, int newIndex) async { + void updateIsarIndex(int id, int newIndex) async { IsarService.instance.addPreviousPlaylistCurrentIndex( PreviousPlaylistCurrentIndex(mediaId: id, currentIndex: newIndex), ); @@ -347,7 +347,7 @@ class Queue { Media? item(int pos) { final item = storage[pos].item; - _updateIndex(item.id, pos); + updateIsarIndex(item.id, pos); if (storage.length == 0) { return null; } else if (storage.length > 0 && pos <= storage.length - 1) { From 004d6964f8c5866b56ccc8d9c1febba204c0be56 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 13 Sep 2024 19:27:16 -0300 Subject: [PATCH 36/70] fix isar, int overflow, stopself when task removed --- .../kotlin/br/com/suamusica/player/Media.kt | 6 ++--- .../player/MediaButtonEventHandler.kt | 2 +- .../br/com/suamusica/player/MediaService.kt | 26 ++++++++++++------- .../player/MediaSessionConnection.kt | 2 +- .../br/com/suamusica/player/PlayerPlugin.kt | 2 +- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt index f084caf0..13dfbb5c 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Media.kt @@ -3,10 +3,10 @@ package br.com.suamusica.player import com.google.gson.annotations.SerializedName data class Media( - @SerializedName("id") val id: Int, + @SerializedName("id") val id: Long, @SerializedName("name") val name: String, @SerializedName("ownerId") val ownerId: Int, - @SerializedName("albumId") val albumId: Int, + @SerializedName("albumId") val albumId: Long, @SerializedName("albumTitle") val albumTitle: String, @SerializedName("author") val author: String, @SerializedName("url") val url: String, @@ -15,7 +15,7 @@ data class Media( @SerializedName("bigCover") val bigCoverUrl: String, @SerializedName("is_verified") val isVerified: Boolean, @SerializedName("shared_url") val shareUrl: String, - @SerializedName("playlist_id") val playlistId: Int, + @SerializedName("playlist_id") val playlistId: Long, @SerializedName("is_spot") val isSpot: Boolean, @SerializedName("isFavorite") val isFavorite: Boolean?, @SerializedName("fallbackUrl") val fallbackUrl: String, 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 e4d72675..58624d8e 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 @@ -135,7 +135,7 @@ class MediaButtonEventHandler( } if (customCommand.customAction == REMOVE_IN) { mediaService.removeIn( - args.getIntegerArrayList(INDEXES_TO_REMOVE)?.toList() ?: emptyList() + args.getIntegerArrayList(INDEXES_TO_REMOVE) ?: emptyList() ) } if (customCommand.customAction == REORDER) { 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 24402983..577d21b0 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 @@ -186,14 +186,7 @@ class MediaService : MediaSessionService() { ): MediaSession = mediaSession override fun onTaskRemoved(rootIntent: Intent?) { - val player = mediaSession.player - val shouldStopService = !player.playWhenReady - || player.mediaItemCount == 0 - || player.playbackState == Player.STATE_ENDED - if (shouldStopService) { - stopSelf() - } - isServiceRunning() + stopSelf() } override fun onDestroy() { @@ -283,6 +276,7 @@ class MediaService : MediaSessionService() { } player?.addMediaSources(mediaSources) player?.prepare() + PlayerSingleton.playerChangeNotifier?.notifyItemTransition() } } @@ -348,10 +342,22 @@ class MediaService : MediaSessionService() { if (indexes.isNotEmpty()) { indexes.forEach { player?.removeMediaItem(it) - shuffleOrder?.cloneAndRemove(it, it) - shuffledIndices.removeAt(it) + if (shuffledIndices.isNotEmpty()) { + shuffledIndices.removeAt( + shuffledIndices.indexOf( + player?.currentMediaItemIndex ?: 0 + ) + ) + } } } + if (player?.shuffleModeEnabled == true) { + shuffleOrder = DefaultShuffleOrder( + shuffledIndices.toIntArray(), + System.currentTimeMillis() + ) + player?.setShuffleOrder(shuffleOrder!!) + } } fun disableRepeatMode() { 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 5be98e22..d72dbc95 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 @@ -117,7 +117,7 @@ class MediaSessionConnection( fun removeIn(indexes: List) { val bundle = Bundle() bundle.putIntegerArrayList(INDEXES_TO_REMOVE, ArrayList(indexes)) - sendCommand(REMOVE_IN, null) + sendCommand(REMOVE_IN, bundle) } fun next() { 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 d59d9930..ef3b42f8 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 @@ -25,7 +25,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val ID_FAVORITE_ARGUMENT = "idFavorite" const val POSITION_ARGUMENT = "position" const val TIME_POSITION_ARGUMENT = "timePosition" - const val INDEXES_TO_REMOVE = "indexesToRemove" + const val INDEXES_TO_REMOVE = "indexesToDelete" const val POSITIONS_LIST = "positionsList" const val LOAD_ONLY = "loadOnly" const val RELEASE_MODE_ARGUMENT = "releaseMode" From f7021a42bfcd18b662d1c35ba7d895310049fb8f Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 17 Sep 2024 09:47:51 -0300 Subject: [PATCH 37/70] fix remove in and playfromqueue --- .../player/MediaButtonEventHandler.kt | 10 +-- .../br/com/suamusica/player/MediaService.kt | 41 ++++++++---- .../player/MediaSessionConnection.kt | 1 - .../suamusica/player/MethodChannelManager.kt | 13 ---- .../suamusica/player/PlayerChangeNotifier.kt | 9 +-- .../br/com/suamusica/player/PlayerPlugin.kt | 6 +- .../example/.flutter-plugins-dependencies | 2 +- packages/player/lib/src/player.dart | 62 +++++++++---------- 8 files changed, 69 insertions(+), 75 deletions(-) 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 58624d8e..2fc1d8b2 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 @@ -121,12 +121,13 @@ class MediaButtonEventHandler( mediaService.seek(args.getLong("position"), args.getBoolean("playWhenReady")) } if (customCommand.customAction == FAVORITE) { + val isFavorite = args.getBoolean(IS_FAVORITE_ARGUMENT) val mediaItem = session.player.currentMediaItem!! updateFavoriteMetadata( session.player, session.player.currentMediaItemIndex, mediaItem, - args.getBoolean(IS_FAVORITE_ARGUMENT) + isFavorite, ) buildIcons() } @@ -207,11 +208,12 @@ class MediaButtonEventHandler( PlayerSingleton.favorite(isFavorite) // } } - if (customCommand.customAction == "ads_playing" || customCommand.customAction == "remove_notification") { + if (customCommand.customAction == "ads_playing") { +// mediaService.player?.pause() // mediaService.adsPlaying() - mediaService.removeNotification() +// mediaService.removeNotification() } - if (customCommand.customAction == ENQUEUE || customCommand.customAction == ENQUEUE_ONE) { + if (customCommand.customAction == ENQUEUE) { val json = args.getString("json") val gson = GsonBuilder().create() val mediaListType = object : TypeToken>() {}.type 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 577d21b0..0748fdf2 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 @@ -93,7 +93,7 @@ class MediaService : MediaSessionService() { private lateinit var mediaButtonEventHandler: MediaButtonEventHandler private var shuffleOrder: DefaultShuffleOrder? = null - private val artCache = HashMap() + private var seekToLoadOnly: Boolean = false private var shuffledIndices = mutableListOf() private var autoPlay: Boolean = true @@ -186,6 +186,8 @@ class MediaService : MediaSessionService() { ): MediaSession = mediaSession override fun onTaskRemoved(rootIntent: Intent?) { + Log.d(TAG, "onTaskRemoved") + player?.stop() stopSelf() } @@ -276,7 +278,7 @@ class MediaService : MediaSessionService() { } player?.addMediaSources(mediaSources) player?.prepare() - PlayerSingleton.playerChangeNotifier?.notifyItemTransition() +// PlayerSingleton.playerChangeNotifier?.notifyItemTransition("createMediaSource") } } @@ -300,7 +302,6 @@ class MediaService : MediaSessionService() { .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) .setAllowChunklessPreparation(true) .createMediaSource(mediaItem) - } C.CONTENT_TYPE_OTHER -> { @@ -339,8 +340,13 @@ class MediaService : MediaSessionService() { } fun removeIn(indexes: List) { - if (indexes.isNotEmpty()) { - indexes.forEach { + val sortedIndexes = indexes.sortedDescending() + if (sortedIndexes.isNotEmpty()) { + sortedIndexes.forEach { + android.util.Log.d( + "#NATIVE LOGS ==>", + "removeIn ${player?.getMediaItemAt(it)?.mediaMetadata?.title}" + ) player?.removeMediaItem(it) if (shuffledIndices.isNotEmpty()) { shuffledIndices.removeAt( @@ -412,6 +418,10 @@ class MediaService : MediaSessionService() { fun playFromQueue(position: Int, timePosition: Long, loadOnly: Boolean = false) { player?.playWhenReady = !loadOnly + if (loadOnly) { + seekToLoadOnly = true + } + player?.seekTo( if (player?.shuffleModeEnabled == true) shuffledIndices[position] else position, timePosition, @@ -421,9 +431,6 @@ class MediaService : MediaSessionService() { } } - fun removeNotification() { - } - fun removeAll() { player?.stop() player?.clearMediaItems() @@ -547,6 +554,10 @@ class MediaService : MediaSessionService() { ) { super.onMediaItemTransition(mediaItem, reason) Log.d(TAG, "onMediaItemTransition: reason: ${reason}") +// if(isSeekWithLoadOnly){ +// isSeekWithLoadOnly = false +// return +// } if ((player?.mediaItemCount ?: 0) > 0 // && intArrayOf( // Player.MEDIA_ITEM_TRANSITION_REASON_AUTO, @@ -554,13 +565,17 @@ class MediaService : MediaSessionService() { // ).contains(reason) ) { PlayerSingleton.playerChangeNotifier?.currentMediaIndex( - currentIndex() + currentIndex(), + "onMediaItemTransition", ) } mediaButtonEventHandler.buildIcons() if (reason != Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) { - player?.playWhenReady = true - PlayerSingleton.playerChangeNotifier?.notifyItemTransition() + if (!seekToLoadOnly) { + player?.playWhenReady = true + seekToLoadOnly = false + } + PlayerSingleton.playerChangeNotifier?.notifyItemTransition("onMediaItemTransition != 3") } } @@ -590,6 +605,10 @@ class MediaService : MediaSessionService() { } override fun onPlayerError(error: PlaybackException) { + android.util.Log.d( + "#NATIVE LOGS ==>", + "onPlayerError cause ${error.cause.toString()}" + ) val bundle = Bundle() bundle.putString("type", "error") bundle.putString( 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 d72dbc95..7fb73bd9 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 @@ -229,7 +229,6 @@ class MediaSessionConnection( var lastState = PlaybackStateCompat.STATE_NONE - 1 override fun onPlaybackStateChanged(state: PlaybackStateCompat) { if (lastState != state.state) { - Log.i(TAG, "onPlaybackStateChanged2: $state") lastState = state.state playerChangeNotifier.notifyStateChange(state.state) } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt index 25982e6d..1fcd8891 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt @@ -44,19 +44,6 @@ class MethodChannelManager(private val channel: MethodChannel) { invokeMethod("state.change", args) } - fun sendCurrentQueue( - idSum: Long, - playerId: String, - ) { - val args = MethodChannelManagerArgsBuilder() - .event("CURRENT_QUEUE") - .playerId(playerId) - .idSum(idSum) - .build() - invokeMethod("GET_INFO", args) - } - - private fun invokeMethod(method: String, args: Map) { channel.invokeMethod(method, args) } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index 1448e2c0..205a0ff9 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -31,16 +31,11 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { Log.i("Player", "Notifying Player Previous") channelManager.notifyPrevious("sua-musica-player") } - fun notifyItemTransition() { + fun notifyItemTransition(from:String) { Log.i("Player", "notifyItemTransition") channelManager.notifyItemTransition("sua-musica-player") } - fun sendCurrentQueue(idSum: Long) { - Log.i("Player", "Notifying Player Previous") - channelManager.sendCurrentQueue(idSum, "sua-musica-player") - } - fun currentMediaIndex(currentMediaIndex: Int) { - Log.i("Player", "Notifying Player Previous") + fun currentMediaIndex(currentMediaIndex: Int, from: String) { channelManager.currentMediaIndex("sua-musica-player", currentMediaIndex) } fun notifyPositionChange(position: Long, duration: Long) { 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 ef3b42f8..f84e8333 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 @@ -114,13 +114,13 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { } private fun handleMethodCall(call: MethodCall, response: MethodChannel.Result) { - if (call.method == ENQUEUE || call.method == ENQUEUE_ONE) { + if (call.method == ENQUEUE) { val batch: Map = call.arguments()!! cookie = if (batch.containsKey("cookie")) batch["cookie"] as String else cookie PlayerSingleton.externalPlayback = if (batch.containsKey("externalplayback")) batch["externalplayback"].toString() == "true" else PlayerSingleton.externalPlayback } else { - cookie = call.argument("cookie") ?: "" + cookie = call.argument("cookie") ?: cookie PlayerSingleton.externalPlayback = call.argument("externalplayback") } Log.d( @@ -128,8 +128,6 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { "method: ${call.method}" ) when (call.method) { - LOAD_METHOD -> {} - ENQUEUE_ONE, ENQUEUE -> { val batch: Map = call.arguments()!! val listMedia: List> = diff --git a/packages/player/example/.flutter-plugins-dependencies b/packages/player/example/.flutter-plugins-dependencies index 46a24c2e..ca9c0226 100644 --- a/packages/player/example/.flutter-plugins-dependencies +++ b/packages/player/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"isar_flutter_libs","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/suamusica/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","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.10/","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.4.0/","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.3.0/","native_build":false,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"isar_flutter_libs","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider"]}],"date_created":"2024-08-26 15:15:59.101278","version":"3.24.0","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"android":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_android-2.2.10/","native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"macos":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"isar_flutter_libs","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider"]}],"date_created":"2024-09-14 19:45:21.353811","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index b3ad3c77..cb786515 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:smaws/aws.dart'; import 'package:flutter/services.dart'; +import 'package:smplayer/src/before_play_event.dart'; import 'package:smplayer/src/event.dart'; import 'package:smplayer/src/event_type.dart'; import 'package:smplayer/src/isar_service.dart'; @@ -117,10 +118,6 @@ class Player { Future enqueueAll( List items, { bool autoPlay = false, - // Future Function( - // int albumId, - // String url, - // )? getLocalCover, }) async { _cookies = await cookieSigner(); String cookie = _cookies!.toHeaders(); @@ -244,8 +241,11 @@ class Player { int get currentIndex => _queue.index; Future play({bool shouldPrepare = false}) async { - _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); - return _doPlay(shouldPrepare: shouldPrepare); + _channel.invokeMethod( + 'play', + {'shouldPrepare': shouldPrepare}, + ); + return Ok; } Future playFromQueue( @@ -287,12 +287,6 @@ class Player { return result; } - Future rewind() async { - final media = _queue.rewind(); - _notifyRewind(media); - return Ok; - } - List> getPositionsList() { return [ for (var item in _queue.storage) @@ -382,15 +376,16 @@ class Player { void addUsingPlayer(Event event) => _addUsingPlayer(player, event); Future stop() async { - _notifyPlayerStatusChangeEvent(EventType.STOP_REQUESTED); - final int result = await _invokeMethod('stop'); + // _notifyPlayerStatusChangeEvent(EventType.STOP_REQUESTED); + // final int result = await _invokeMethod('stop'); - if (result == Ok) { - state = PlayerState.STOPPED; - _notifyPlayerStatusChangeEvent(EventType.STOPPED); - } + // if (result == Ok) { + // state = PlayerState.STOPPED; + // _notifyPlayerStatusChangeEvent(EventType.STOPPED); + // } - return result; + // return result; + return Ok; } Future resume() async { @@ -675,8 +670,7 @@ class Player { break; case 'SET_CURRENT_MEDIA_INDEX': _queue.setIndex = callArgs['CURRENT_MEDIA_INDEX']; - _queue.updateIsarIndex( - currentMedia!.id, callArgs['CURRENT_MEDIA_INDEX']); + _queue.updateIsarIndex(currentMedia!.id, _queue.index); _notifyPlayerStateChangeEvent( player, EventType.SET_CURRENT_MEDIA_INDEX, @@ -704,19 +698,19 @@ class Player { } } - _notifyRewind(Media media) async { - final positionInMilli = await getCurrentPosition(); - final durationInMilli = await getDuration(); - _add( - Event( - type: EventType.REWIND, - media: media, - queuePosition: currentIndex, - position: Duration(milliseconds: positionInMilli), - duration: Duration(milliseconds: durationInMilli), - ), - ); - } + // _notifyRewind(Media media) async { + // final positionInMilli = await getCurrentPosition(); + // final durationInMilli = await getDuration(); + // _add( + // Event( + // type: EventType.REWIND, + // media: media, + // queuePosition: currentIndex, + // position: Duration(milliseconds: positionInMilli), + // duration: Duration(milliseconds: durationInMilli), + // ), + // ); + // } _notifyForward(Media media) async { final positionInMilli = await getCurrentPosition(); From aa5c713972e38854e20f5e547e151b0a054e6d05 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 18 Sep 2024 16:48:00 -0300 Subject: [PATCH 38/70] added set repeat mode and update mediaItem with fallback --- .../player/MediaButtonEventHandler.kt | 6 + .../br/com/suamusica/player/MediaService.kt | 181 ++++++++++++------ .../player/MediaSessionConnection.kt | 59 +----- .../suamusica/player/MethodChannelManager.kt | 10 +- .../suamusica/player/PlayerChangeNotifier.kt | 26 ++- .../br/com/suamusica/player/PlayerPlugin.kt | 7 + .../br/com/suamusica/player/PlayerState.kt | 3 +- packages/player/lib/src/event_type.dart | 2 + packages/player/lib/src/player.dart | 21 +- packages/player/lib/src/player_state.dart | 1 + 10 files changed, 189 insertions(+), 127 deletions(-) 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 2fc1d8b2..43cfa36b 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 @@ -35,6 +35,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN import br.com.suamusica.player.PlayerPlugin.Companion.REORDER import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.SET_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE @@ -80,6 +81,7 @@ class MediaButtonEventHandler( add(SessionCommand(REMOVE_ALL, Bundle.EMPTY)) add(SessionCommand(REORDER, session.token.extras)) add(SessionCommand(REMOVE_IN, session.token.extras)) + add(SessionCommand(SET_REPEAT_MODE, session.token.extras)) add(SessionCommand("prepare", session.token.extras)) add(SessionCommand("playFromQueue", session.token.extras)) add(SessionCommand("play", Bundle.EMPTY)) @@ -173,6 +175,10 @@ class MediaButtonEventHandler( val shouldPrepare = args.getBoolean("shouldPrepare") mediaService.play(shouldPrepare) } + if (customCommand.customAction == SET_REPEAT_MODE) { + val mode = args.getString("mode") + mediaService.setRepeatMode(mode ?:"") + } if (customCommand.customAction == PLAY_FROM_QUEUE_METHOD) { mediaService.playFromQueue( args.getInt(POSITION_ARGUMENT), args.getLong(TIME_POSITION_ARGUMENT), 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 0748fdf2..3c5941b3 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 @@ -4,8 +4,6 @@ import android.app.ActivityManager import android.app.PendingIntent import android.app.Service import android.content.Intent -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.net.Uri import android.os.Build import android.os.Bundle @@ -15,19 +13,16 @@ import androidx.media3.common.AudioAttributes import androidx.media3.common.C 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.Player.EVENT_MEDIA_ITEM_TRANSITION import androidx.media3.common.Player.MediaItemTransitionReason import androidx.media3.common.Player.REPEAT_MODE_ALL import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Player.REPEAT_MODE_ONE -import androidx.media3.common.Player.STATE_BUFFERING import androidx.media3.common.Player.STATE_ENDED -import androidx.media3.common.Player.STATE_IDLE -import androidx.media3.common.Player.STATE_READY import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util @@ -39,7 +34,6 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource -import androidx.media3.exoplayer.source.ShuffleOrder import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder import androidx.media3.session.CacheBitmapLoader import androidx.media3.session.CommandButton @@ -48,23 +42,21 @@ 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.PlayerPlugin.Companion.FALLBACK_URL import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.cookie +import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.ByteArrayOutputStream import java.io.File import java.util.Collections -import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" @@ -99,7 +91,7 @@ class MediaService : MediaSessionService() { private val channel = Channel>(Channel.BUFFERED) private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - + private var currentMedias = listOf() override fun onCreate() { super.onCreate() @@ -111,6 +103,10 @@ class MediaService : MediaSessionService() { addListener(playerEventListener()) setWakeMode(C.WAKE_MODE_NETWORK) setHandleAudioBecomingNoisy(true) + preloadConfiguration = ExoPlayer.PreloadConfiguration( + 10000000 + ) + } dataSourceBitmapLoader = @@ -188,6 +184,7 @@ class MediaService : MediaSessionService() { override fun onTaskRemoved(rootIntent: Intent?) { Log.d(TAG, "onTaskRemoved") player?.stop() + stopTrackingProgress() stopSelf() } @@ -235,7 +232,7 @@ class MediaService : MediaSessionService() { ) player!!.setShuffleOrder(shuffleOrder!!) } - PlayerSingleton.playerChangeNotifier?.onShuffleModeEnabled(it) + playerChangeNotifier?.onShuffleModeEnabled(it) } } @@ -246,7 +243,7 @@ class MediaService : MediaSessionService() { } private fun processItem(item: List) { - createMediaSource(cookie, item, autoPlay) + createMediaSource(cookie, item) } private val consumer = serviceScope.launch { @@ -267,10 +264,11 @@ class MediaService : MediaSessionService() { if (player?.mediaItemCount == 0) { player?.playWhenReady = autoPlay } + currentMedias = medias addToQueue(medias) } - private fun createMediaSource(cookie: String, medias: List, autoPlay: Boolean) { + private fun createMediaSource(cookie: String, medias: List) { val mediaSources: MutableList = mutableListOf() if (medias.isNotEmpty()) { for (i in medias.indices) { @@ -279,17 +277,21 @@ class MediaService : MediaSessionService() { player?.addMediaSources(mediaSources) player?.prepare() // PlayerSingleton.playerChangeNotifier?.notifyItemTransition("createMediaSource") + playerChangeNotifier?.currentMediaIndex( + currentIndex(), + "createMediaSource", + ) } } - private fun prepare(cookie: String, media: Media, coverBytes: ByteArray? = null): MediaSource { + private fun prepare(cookie: String, media: Media, urlToPrepare: String): MediaSource { val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setReadTimeoutMs(15 * 1000) dataSourceFactory.setConnectTimeoutMs(10 * 1000) dataSourceFactory.setUserAgent(userAgent) dataSourceFactory.setAllowCrossProtocolRedirects(true) dataSourceFactory.setDefaultRequestProperties(mapOf("Cookie" to cookie)) - val metadata = buildMetaData(media, coverBytes) + val metadata = buildMetaData(media) val url = media.url val uri = if (url.startsWith("/")) Uri.fromFile(File(url)) else Uri.parse(url) val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata) @@ -388,11 +390,12 @@ class MediaService : MediaSessionService() { } } - private fun buildMetaData(media: Media, coverBytes: ByteArray?): MediaMetadata { + private fun buildMetaData(media: Media): MediaMetadata { val metadataBuilder = MediaMetadata.Builder() val bundle = Bundle() bundle.putBoolean(IS_FAVORITE_ARGUMENT, media.isFavorite ?: false) + bundle.putString(FALLBACK_URL, media.fallbackUrl) metadataBuilder.apply { setAlbumTitle(media.name) setArtist(media.author) @@ -415,6 +418,15 @@ class MediaService : MediaSessionService() { } } + fun setRepeatMode(mode: String) { + player?.repeatMode = when (mode) { + "off" -> REPEAT_MODE_OFF + "one" -> REPEAT_MODE_ONE + "all" -> REPEAT_MODE_ALL + else -> REPEAT_MODE_OFF + } + } + fun playFromQueue(position: Int, timePosition: Long, loadOnly: Boolean = false) { player?.playWhenReady = !loadOnly @@ -429,6 +441,7 @@ class MediaService : MediaSessionService() { if (!loadOnly) { player?.prepare() } + playerChangeNotifier?.currentMediaIndex(currentIndex(),"playFromQueue") } fun removeAll() { @@ -472,14 +485,7 @@ class MediaService : MediaSessionService() { var position = player?.currentPosition ?: 0L val duration = player?.duration ?: 0L position = if (position > duration) duration else position - - if (duration > 0) { - val extra = Bundle() - extra.putString("type", "position") - extra.putLong("position", position) - extra.putLong("duration", duration) - mediaSession.setSessionExtras(extra) - } + playerChangeNotifier?.notifyPositionChange(position, duration) } private fun startTrackingProgress() { @@ -532,76 +538,111 @@ class MediaService : MediaSessionService() { reason: Int ) { if (reason == DISCONTINUITY_REASON_SEEK) { - val bundle = Bundle() - bundle.putString("type", "seek-end") - mediaSession.setSessionExtras(bundle) + playerChangeNotifier?.notifySeekEnd() } } override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) + playerChangeNotifier?.notifyPlaying(isPlaying) if (isPlaying) { // PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PLAYING) startTrackingProgress() } else { +// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PAUSED) stopTrackingProgressAndPerformTask {} } } + override fun onEvents(player: Player, events: Player.Events) { + super.onEvents(player, events) + for (i in 0 until events.size()) { + val event = events.get(i) + val eventName = when (event) { + Player.EVENT_TIMELINE_CHANGED -> "EVENT_TIMELINE_CHANGED" + Player.EVENT_MEDIA_ITEM_TRANSITION -> "EVENT_MEDIA_ITEM_TRANSITION" + Player.EVENT_TRACKS_CHANGED -> "EVENT_TRACKS_CHANGED" + Player.EVENT_IS_LOADING_CHANGED -> "EVENT_IS_LOADING_CHANGED" + Player.EVENT_PLAYBACK_STATE_CHANGED -> "EVENT_PLAYBACK_STATE_CHANGED" + Player.EVENT_PLAY_WHEN_READY_CHANGED -> "EVENT_PLAY_WHEN_READY_CHANGED" + Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED -> "EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED" + Player.EVENT_IS_PLAYING_CHANGED -> "EVENT_IS_PLAYING_CHANGED" + Player.EVENT_REPEAT_MODE_CHANGED -> "EVENT_REPEAT_MODE_CHANGED" + Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED -> "EVENT_SHUFFLE_MODE_ENABLED_CHANGED" + Player.EVENT_PLAYER_ERROR -> "EVENT_PLAYER_ERROR" + Player.EVENT_POSITION_DISCONTINUITY -> "EVENT_POSITION_DISCONTINUITY" + Player.EVENT_PLAYBACK_PARAMETERS_CHANGED -> "EVENT_PLAYBACK_PARAMETERS_CHANGED" + Player.EVENT_AVAILABLE_COMMANDS_CHANGED -> "EVENT_AVAILABLE_COMMANDS_CHANGED" + Player.EVENT_MEDIA_METADATA_CHANGED -> "EVENT_MEDIA_METADATA_CHANGED" + Player.EVENT_PLAYLIST_METADATA_CHANGED -> "EVENT_PLAYLIST_METADATA_CHANGED" + Player.EVENT_SEEK_BACK_INCREMENT_CHANGED -> "EVENT_SEEK_BACK_INCREMENT_CHANGED" + Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED -> "EVENT_SEEK_FORWARD_INCREMENT_CHANGED" + Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED -> "EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED" + Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED -> "EVENT_TRACK_SELECTION_PARAMETERS_CHANGED" + Player.EVENT_AUDIO_ATTRIBUTES_CHANGED -> "EVENT_AUDIO_ATTRIBUTES_CHANGED" + Player.EVENT_AUDIO_SESSION_ID -> "EVENT_AUDIO_SESSION_ID" + Player.EVENT_VOLUME_CHANGED -> "EVENT_VOLUME_CHANGED" + Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED -> "EVENT_SKIP_SILENCE_ENABLED_CHANGED" + Player.EVENT_SURFACE_SIZE_CHANGED -> "EVENT_SURFACE_SIZE_CHANGED" + Player.EVENT_VIDEO_SIZE_CHANGED -> "EVENT_VIDEO_SIZE_CHANGED" + Player.EVENT_RENDERED_FIRST_FRAME -> "EVENT_RENDERED_FIRST_FRAME" + Player.EVENT_CUES -> "EVENT_CUES" + Player.EVENT_METADATA -> "EVENT_METADATA" + Player.EVENT_DEVICE_VOLUME_CHANGED -> "EVENT_DEVICE_VOLUME_CHANGED" + Player.EVENT_DEVICE_INFO_CHANGED -> "EVENT_DEVICE_INFO_CHANGED" + else -> "UNKNOWN_EVENT" + } + Log.d(TAG, "LOG onEvents: reason: $eventName") + if (event == Player.EVENT_MEDIA_METADATA_CHANGED) { + playerChangeNotifier?.notifyPlaying(player.isPlaying) + } + } + } + override fun onMediaItemTransition( mediaItem: MediaItem?, reason: @MediaItemTransitionReason Int ) { super.onMediaItemTransition(mediaItem, reason) Log.d(TAG, "onMediaItemTransition: reason: ${reason}") -// if(isSeekWithLoadOnly){ -// isSeekWithLoadOnly = false -// return -// } - if ((player?.mediaItemCount ?: 0) > 0 -// && intArrayOf( -// Player.MEDIA_ITEM_TRANSITION_REASON_AUTO, -// Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -// ).contains(reason) - ) { - PlayerSingleton.playerChangeNotifier?.currentMediaIndex( - currentIndex(), - "onMediaItemTransition", - ) - } + playerChangeNotifier?.currentMediaIndex( + currentIndex(), + "onMediaItemTransition", + ) mediaButtonEventHandler.buildIcons() if (reason != Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) { if (!seekToLoadOnly) { player?.playWhenReady = true seekToLoadOnly = false } - PlayerSingleton.playerChangeNotifier?.notifyItemTransition("onMediaItemTransition != 3") + playerChangeNotifier?.notifyItemTransition("onMediaItemTransition != 3 seekToLoadOnly: $seekToLoadOnly") } } + var lastState = PlaybackStateCompat.STATE_NONE - 1 + override fun onPlaybackStateChanged(playbackState: @Player.State Int) { super.onPlaybackStateChanged(playbackState) + if (lastState != playbackState) { + lastState = playbackState + playerChangeNotifier?.notifyStateChange(playbackState) + } -// if(playbackState == STATE_IDLE) { -// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_NONE) -// } else if(playbackState == STATE_BUFFERING) { -// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_BUFFERING) -// } else if(playbackState == STATE_READY) { -// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PLAYING) -// startTrackingProgress() -// } else if(playbackState == STATE_ENDED) { -// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_STOPPED) -// } -// Log.d(TAG, "#onPlaybackStateChanged: playbackState: $playbackState") -// PlayerSingleton.playerChangeNotifier?.notifyStateChange(playbackState) if (playbackState == STATE_ENDED) { stopTrackingProgressAndPerformTask {} } + Log.d(TAG, "##onPlaybackStateChanged $playbackState") + } + + override fun onPlayerErrorChanged(error: PlaybackException?) { + super.onPlayerErrorChanged(error) + Log.d(TAG, "##onPlayerErrorChanged ${error}") + } override fun onRepeatModeChanged(repeatMode: @Player.RepeatMode Int) { super.onRepeatModeChanged(repeatMode) - PlayerSingleton.playerChangeNotifier?.onRepeatChanged(repeatMode) + playerChangeNotifier?.onRepeatChanged(repeatMode) } override fun onPlayerError(error: PlaybackException) { @@ -609,15 +650,29 @@ class MediaService : MediaSessionService() { "#NATIVE LOGS ==>", "onPlayerError cause ${error.cause.toString()}" ) - val bundle = Bundle() - bundle.putString("type", "error") - bundle.putString( - "error", + + if (error.cause.toString() + .contains("No such file or directory") + ) { + val mediaItem = player?.currentMediaItem!! + player?.removeMediaItem(player?.currentMediaItemIndex ?: 0) + player?.addMediaSource( + player?.currentMediaItemIndex ?: 0, prepare( + cookie, + currentMedias[player?.currentMediaItemIndex ?: 0], + mediaItem.mediaMetadata.extras?.getString(FALLBACK_URL) ?: "" + ) + ) + player?.prepare() + playFromQueue(currentIndex()-1, 0) + return + } + + playerChangeNotifier?.notifyError( if (error.cause.toString() .contains("Permission denied") ) "Permission denied" else error.message ) - mediaSession.setSessionExtras(bundle) } override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { 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 7fb73bd9..bf469263 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 @@ -23,6 +23,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN import br.com.suamusica.player.PlayerPlugin.Companion.REORDER import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.SET_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE @@ -61,14 +62,14 @@ class MediaSessionConnection( } } - fun enqueue( medias: String, autoPlay: Boolean) { + fun enqueue(medias: String, autoPlay: Boolean) { val bundle = Bundle() bundle.putString("json", medias) bundle.putBoolean("autoPlay", autoPlay) sendCommand(ENQUEUE, bundle) } - fun playFromQueue(index: Int, timePosition:Long, loadOnly:Boolean) { + fun playFromQueue(index: Int, timePosition: Long, loadOnly: Boolean) { val bundle = Bundle() bundle.putInt(POSITION_ARGUMENT, index) bundle.putLong(TIME_POSITION_ARGUMENT, timePosition) @@ -82,6 +83,12 @@ class MediaSessionConnection( sendCommand("play", bundle) } + fun setRepeatMode(mode: String) { + val bundle = Bundle() + bundle.putString("mode", mode) + sendCommand(SET_REPEAT_MODE, bundle) + } + fun reorder(oldIndex: Int, newIndex: Int, positionsList: List>) { val bundle = Bundle() bundle.putInt("oldIndex", oldIndex) @@ -225,54 +232,6 @@ class MediaSessionConnection( return mediaController = MediaControllerCompat(context, mediaBrowser.sessionToken) - mediaController?.registerCallback(object : MediaControllerCompat.Callback() { - var lastState = PlaybackStateCompat.STATE_NONE - 1 - override fun onPlaybackStateChanged(state: PlaybackStateCompat) { - if (lastState != state.state) { - lastState = state.state - playerChangeNotifier.notifyStateChange(state.state) - } - } - - override fun onExtrasChanged(extras: Bundle) { - if (extras.containsKey("type")) { - when (extras.getString("type")) { - "position" -> { - val position = extras.getLong("position") - this@MediaSessionConnection.currentPosition = position - val duration = extras.getLong("duration") - this@MediaSessionConnection.duration = duration - playerChangeNotifier.notifyPositionChange(position, duration) - } - - "error" -> { - val error = extras.getString("error") - playerChangeNotifier.notifyStateChange( - PlaybackStateCompat.STATE_ERROR, - error - ) - } - - "seek-end" -> { - playerChangeNotifier.notifySeekEnd() - } - - "next" -> { - playerChangeNotifier.notifyNext() - } - - "previous" -> { - playerChangeNotifier.notifyPrevious() - } - } - } - super.onExtrasChanged(extras) - } - - override fun onMetadataChanged(metadata: MediaMetadataCompat) { -// Log.i(TAG, "onMetadataChanged: $metadata duration: ${metadata.duration}") - } - }) } Log.i(TAG, "MediaBrowserConnectionCallback.onConnected : ENDED") } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt index 1fcd8891..0c0dfaf2 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManager.kt @@ -14,7 +14,15 @@ class MethodChannelManager(private val channel: MethodChannel) { invokeMethod("audio.onCurrentPosition", args) } - fun notifyPlayerStateChange(playerId: String, state: PlayerState, error: String? = null) { + fun notifyPlayerStateChange(playerId: String, state: PlayerState) { + val args = ArgsBuilder() + .playerId(playerId) + .state(state) + .build() + + invokeMethod("state.change", args) + } + fun notifyError(playerId: String, state: PlayerState, error: String? = null) { val args = ArgsBuilder() .playerId(playerId) .state(state) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index 205a0ff9..e92ad134 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -2,20 +2,28 @@ package br.com.suamusica.player import android.support.v4.media.session.PlaybackStateCompat import android.util.Log +import androidx.media3.common.Player +import androidx.media3.common.Player.* class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { - fun notifyStateChange(state: Int, error: String? = null) { + fun notifyStateChange(state: @State Int) { val playerState = when (state) { - PlaybackStateCompat.STATE_NONE -> PlayerState.IDLE - PlaybackStateCompat.STATE_BUFFERING -> PlayerState.BUFFERING - PlaybackStateCompat.STATE_PAUSED -> PlayerState.PAUSED - PlaybackStateCompat.STATE_PLAYING -> PlayerState.PLAYING - PlaybackStateCompat.STATE_ERROR -> PlayerState.ERROR - PlaybackStateCompat.STATE_STOPPED -> PlayerState.COMPLETED + STATE_IDLE, STATE_READY -> PlayerState.IDLE + STATE_BUFFERING -> PlayerState.BUFFERING + STATE_ENDED -> PlayerState.COMPLETED + STATE_READY -> PlayerState.STATE_READY else -> PlayerState.IDLE } - Log.i("Player", "Notifying Player State change: $playerState") - channelManager.notifyPlayerStateChange("sua-musica-player", playerState, error) + Log.i("Player", "Notifying Player State change: $playerState | $state") + channelManager.notifyPlayerStateChange("sua-musica-player", playerState) + } + + fun notifyPlaying(isPlaying:Boolean){ + channelManager.notifyPlayerStateChange("sua-musica-player", if(isPlaying) PlayerState.PLAYING else PlayerState.PAUSED) + } + + fun notifyError(message: String? = null){ + channelManager.notifyError("sua-musica-player", PlayerState.ERROR, message) } fun notifySeekEnd() { 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 f84e8333..b3687fc7 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 @@ -22,6 +22,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val BIG_COVER_URL_ARGUMENT = "bigCoverUrl" const val IS_PLAYING_ARGUMENT = "isPlaying" const val IS_FAVORITE_ARGUMENT = "isFavorite" + const val FALLBACK_URL = "fallbackURL" const val ID_FAVORITE_ARGUMENT = "idFavorite" const val POSITION_ARGUMENT = "position" const val TIME_POSITION_ARGUMENT = "timePosition" @@ -35,6 +36,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { // Method names const val LOAD_METHOD = "load" const val PLAY_METHOD = "play" + const val SET_REPEAT_MODE = "set_repeat_mode" const val ENQUEUE = "enqueue" const val ENQUEUE_ONE = "enqueue_one" const val REMOVE_ALL = "remove_all" @@ -145,6 +147,11 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { PlayerSingleton.mediaSessionConnection?.play(shouldPrepare) } + SET_REPEAT_MODE -> { + val mode = call.argument("mode") ?: "" + PlayerSingleton.mediaSessionConnection?.setRepeatMode(mode) + } + REORDER -> { val from = call.argument("oldIndex") val to = call.argument("newIndex") diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt index 79efe8bb..631d10f6 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt @@ -10,5 +10,6 @@ enum class PlayerState { ERROR, SEEK_END, BUFFER_EMPTY, - ITEM_TRANSITION + ITEM_TRANSITION, + STATE_READY, } \ No newline at end of file diff --git a/packages/player/lib/src/event_type.dart b/packages/player/lib/src/event_type.dart index f1925b99..ef272629 100644 --- a/packages/player/lib/src/event_type.dart +++ b/packages/player/lib/src/event_type.dart @@ -37,6 +37,8 @@ enum EventType { REPEAT_CHANGED, SHUFFLE_CHANGED, STATE_ENDED, + IDLE, + STATE_READY, } enum PlayerErrorType { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index cb786515..5ad6ea09 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -317,6 +317,11 @@ class Player { return _channel.invokeMethod('repeat_mode').then((result) => result); } + Future setRepeatMode(String mode) async { + return _channel.invokeMethod( + 'set_repeat_mode', {'mode': mode}).then((result) => result); + } + Future disableRepeatMode() async { return _channel .invokeMethod('disable_repeat_mode') @@ -324,11 +329,13 @@ class Player { } Future previous() async { - Media? media = _queue.possibleNext(repeatMode); + Media? media = _queue.possiblePrevious(); if (media == null) { return null; } - // final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; + if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { + setRepeatMode("all"); + } _channel.invokeMethod('previous').then((result) => result); return Ok; } @@ -339,7 +346,9 @@ class Player { final media = _queue.possibleNext(repeatMode); if (media != null) { final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; - + if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { + setRepeatMode("all"); + } return _doNext( shallNotify: shallNotify, mediaUrl: mediaUrl, @@ -500,7 +509,13 @@ class Player { _log('state.change call ${PlayerState.values[state]}'); player.state = PlayerState.values[state]; switch (player.state) { + case PlayerState.STATE_READY: case PlayerState.IDLE: + _notifyPlayerStateChangeEvent( + player, + EventType.IDLE, + error, + ); break; case PlayerState.BUFFERING: _notifyPlayerStateChangeEvent( diff --git a/packages/player/lib/src/player_state.dart b/packages/player/lib/src/player_state.dart index 907bd697..c3d51628 100644 --- a/packages/player/lib/src/player_state.dart +++ b/packages/player/lib/src/player_state.dart @@ -10,4 +10,5 @@ enum PlayerState { BUFFER_EMPTY, ITEM_TRANSITION, STATE_ENDED, + STATE_READY, } From 065c5b42c48c8153ed5b808f2353ac9b1a53dde6 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 18 Sep 2024 18:51:19 -0300 Subject: [PATCH 39/70] revert commit --- .../br/com/suamusica/player/MediaService.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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 3c5941b3..7c871ee5 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 @@ -272,7 +272,7 @@ class MediaService : MediaSessionService() { val mediaSources: MutableList = mutableListOf() if (medias.isNotEmpty()) { for (i in medias.indices) { - mediaSources.add(prepare(cookie, medias[i])) + mediaSources.add(prepare(cookie, medias[i],"")) } player?.addMediaSources(mediaSources) player?.prepare() @@ -292,10 +292,15 @@ class MediaService : MediaSessionService() { dataSourceFactory.setAllowCrossProtocolRedirects(true) dataSourceFactory.setDefaultRequestProperties(mapOf("Cookie" to cookie)) val metadata = buildMetaData(media) - val url = media.url - val uri = if (url.startsWith("/")) Uri.fromFile(File(url)) else Uri.parse(url) + val uri = if (urlToPrepare.isEmpty()) { + val url = media.url + if (url.startsWith("/")) Uri.fromFile(File(url)) else Uri.parse(url) + } else { + Uri.parse(urlToPrepare) + } val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata) .setMediaId(media.id.toString()).build() + @C.ContentType val type = Util.inferContentType(uri) return when (type) { @@ -441,7 +446,7 @@ class MediaService : MediaSessionService() { if (!loadOnly) { player?.prepare() } - playerChangeNotifier?.currentMediaIndex(currentIndex(),"playFromQueue") + playerChangeNotifier?.currentMediaIndex(currentIndex(), "playFromQueue") } fun removeAll() { @@ -664,7 +669,7 @@ class MediaService : MediaSessionService() { ) ) player?.prepare() - playFromQueue(currentIndex()-1, 0) + playFromQueue(currentIndex() - 1, 0) return } From 375b52671f4d38286e98984cb01db7a2e8d416c6 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 18 Sep 2024 19:08:19 -0300 Subject: [PATCH 40/70] fix update queue --- .../kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt | 1 + packages/player/lib/src/player.dart | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index e92ad134..ef782041 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -44,6 +44,7 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { channelManager.notifyItemTransition("sua-musica-player") } fun currentMediaIndex(currentMediaIndex: Int, from: String) { + Log.i("Player", "=>> currentMediaIndex | FROM: $from | $currentMediaIndex") channelManager.currentMediaIndex("sua-musica-player", currentMediaIndex) } fun notifyPositionChange(position: Long, duration: Long) { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 5ad6ea09..53f1e013 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -119,6 +119,7 @@ class Player { List items, { bool autoPlay = false, }) async { + _queue.addAll(items); _cookies = await cookieSigner(); String cookie = _cookies!.toHeaders(); final int batchSize = 80; @@ -155,7 +156,7 @@ class Player { } await IsarService.instance.removeAllMusics(); - _queue.addAll(items); + return Ok; } From 35f1c9b46645da8f24acb6a349e795febc1d96e4 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 19 Sep 2024 12:19:48 -0300 Subject: [PATCH 41/70] fix isar --- .../br/com/suamusica/player/MediaButtonEventHandler.kt | 5 +++++ .../src/main/kotlin/br/com/suamusica/player/MediaService.kt | 6 +++++- packages/player/lib/src/player.dart | 4 +--- packages/player/lib/src/queue.dart | 6 ++---- 4 files changed, 13 insertions(+), 8 deletions(-) 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 43cfa36b..2f1b0b81 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 @@ -188,7 +188,12 @@ class MediaButtonEventHandler( ) } if (customCommand.customAction == "notification_previous" || customCommand.customAction == "previous") { + if(session.player.hasPreviousMediaItem()){ + session.player.seekToPreviousMediaItem() + }else{ + session.player.seekToPrevious() + } } if (customCommand.customAction == "notification_next" || customCommand.customAction == "next") { session.player.seekToNextMediaItem() 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 7c871ee5..5d0c5fcc 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 @@ -35,6 +35,7 @@ import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder +import androidx.media3.exoplayer.source.preload.BasePreloadManager import androidx.media3.session.CacheBitmapLoader import androidx.media3.session.CommandButton import androidx.media3.session.DefaultMediaNotificationProvider @@ -106,7 +107,6 @@ class MediaService : MediaSessionService() { preloadConfiguration = ExoPlayer.PreloadConfiguration( 10000000 ) - } dataSourceBitmapLoader = @@ -182,10 +182,12 @@ class MediaService : MediaSessionService() { ): MediaSession = mediaSession override fun onTaskRemoved(rootIntent: Intent?) { + player?.clearMediaItems() Log.d(TAG, "onTaskRemoved") player?.stop() stopTrackingProgress() stopSelf() + super.onTaskRemoved(rootIntent) } override fun onDestroy() { @@ -230,6 +232,7 @@ class MediaService : MediaSessionService() { shuffledIndices.toIntArray(), System.currentTimeMillis() ) + Log.d(TAG, "toggleShuffle - shuffleOrder is null: ${shuffleOrder == null} | shuffledIndices: ${shuffledIndices.size} - ${player?.mediaItemCount}") player!!.setShuffleOrder(shuffleOrder!!) } playerChangeNotifier?.onShuffleModeEnabled(it) @@ -610,6 +613,7 @@ class MediaService : MediaSessionService() { ) { super.onMediaItemTransition(mediaItem, reason) Log.d(TAG, "onMediaItemTransition: reason: ${reason}") + if((player?.mediaItemCount?:0) > 0) playerChangeNotifier?.currentMediaIndex( currentIndex(), "onMediaItemTransition", diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 53f1e013..883442e2 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -155,8 +155,6 @@ class Player { ); } - await IsarService.instance.removeAllMusics(); - return Ok; } @@ -236,7 +234,7 @@ class Player { _queue.previousPosition; List get items => _queue.items; - List get isarItems => _queue.storage.map((e) => e.item).toList(); + List get isarItems => _queue.isarItems; int get size => items.length; int get currentIndex => _queue.index; diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index 5f261896..c24a67c2 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -19,15 +19,13 @@ class Queue { itemsReady = !initializeIsar; _initialize(); } - + List isarItems = []; Future _initialize() async { if (!itemsReady) { try { - final items = await previousItems; + isarItems = await previousItems; previousIndex = await previousPlaylistIndex; previousPosition = await _previousPlaylistPosition; - int i = 0; - storage.addAll(items.map((e) => QueueItem(i++, i, e))); } catch (_) { } finally { itemsReady = true; From 183b8653819604788173f6f307668b543ef93962 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 19 Sep 2024 12:31:10 -0300 Subject: [PATCH 42/70] set repeat mode all in playfrom queue --- packages/player/lib/src/player.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 883442e2..45a476b0 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -255,6 +255,9 @@ class Player { if (!loadOnly) { _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); } + if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { + setRepeatMode("all"); + } return _channel.invokeMethod('playFromQueue', { 'position': pos, 'timePosition': position?.inMilliseconds, From dcb7a8904b069cdf3ec6880321ee79aaf214db01 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 20 Sep 2024 11:22:43 -0300 Subject: [PATCH 43/70] added brackets --- .../br/com/suamusica/player/MediaService.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) 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 5d0c5fcc..25f8b313 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 @@ -23,6 +23,7 @@ import androidx.media3.common.Player.REPEAT_MODE_ALL import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Player.REPEAT_MODE_ONE import androidx.media3.common.Player.STATE_ENDED +import androidx.media3.common.Timeline import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util @@ -39,16 +40,21 @@ import androidx.media3.exoplayer.source.preload.BasePreloadManager import androidx.media3.session.CacheBitmapLoader import androidx.media3.session.CommandButton import androidx.media3.session.DefaultMediaNotificationProvider +import androidx.media3.session.LibraryResult +import androidx.media3.session.MediaConstants import androidx.media3.session.MediaController +import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService +import androidx.media3.session.SessionError import br.com.suamusica.player.PlayerPlugin.Companion.FALLBACK_URL import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.cookie import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -92,7 +98,7 @@ class MediaService : MediaSessionService() { private val channel = Channel>(Channel.BUFFERED) private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - private var currentMedias = listOf() + var currentMedias = listOf() override fun onCreate() { super.onCreate() @@ -613,11 +619,12 @@ class MediaService : MediaSessionService() { ) { super.onMediaItemTransition(mediaItem, reason) Log.d(TAG, "onMediaItemTransition: reason: ${reason}") - if((player?.mediaItemCount?:0) > 0) - playerChangeNotifier?.currentMediaIndex( - currentIndex(), - "onMediaItemTransition", - ) + if((player?.mediaItemCount?:0) > 0) { + playerChangeNotifier?.currentMediaIndex( + currentIndex(), + "onMediaItemTransition", + ) + } mediaButtonEventHandler.buildIcons() if (reason != Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) { if (!seekToLoadOnly) { From c357bcbd077303922b885ef02f99de56ddf77711 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 23 Sep 2024 12:24:26 -0300 Subject: [PATCH 44/70] init covertion to avqueueplayer --- packages/player/ios/Classes/PlayerPlugin.m | 264 ++++++++++-------- .../player/ios/Classes/PlaylistItem.swift | 16 +- 2 files changed, 154 insertions(+), 126 deletions(-) diff --git a/packages/player/ios/Classes/PlayerPlugin.m b/packages/player/ios/Classes/PlayerPlugin.m index c286cf22..f73b1d9b 100644 --- a/packages/player/ios/Classes/PlayerPlugin.m +++ b/packages/player/ios/Classes/PlayerPlugin.m @@ -117,6 +117,8 @@ @implementation PlayerPlugin { BOOL shallSendEvents = true; int wasPlayingBeforeVoiceSearch = -1; +NSMutableArray *playlistItems = nil; + PlaylistItem *currentItem = nil; + (void)registerWithRegistrar:(NSObject*)registrar { @@ -270,7 +272,7 @@ -(void)configureRemoteCommandCenter { if (_playerId != nil) { NSMutableDictionary * playerInfo = players[_playerId]; if ([playerInfo[@"areNotificationCommandsEnabled"] boolValue]) { - AVPlayer *player = playerInfo[@"player"]; + AVQueuePlayer *player = playerInfo[@"player"]; if (player.rate == 0.0) { [self resume:_playerId]; } else { @@ -442,68 +444,81 @@ -(void)setCurrentResourceLoadingRequest: (AVAssetResourceLoadingRequest*) resour - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString * playerId = call.arguments[@"playerId"]; - shallSendEvents = [call.arguments[@"shallSendEvents"] boolValue]; + NSString * playerId = @"call.arguments"; +// shallSendEvents = [call.arguments[@"shallSendEvents"] boolValue]; + NSLog(@"Player: Method Call => call %@, playerId %@", call.method, playerId); typedef void (^CaseBlock)(void); + // Squint and this looks like a proper switch! NSDictionary *methods = @{ @"can_play": ^{ result(@(Ok)); }, - @"load": + @"remove_all":^{ + NSLog(@"Player: remove_all"); + result(@(Ok)); + }, + @"disable_repeat_mode":^{ + NSLog(@"Player: disable_repeat_mode"); + result(@(Ok)); + }, + @"enqueue": ^{ - NSLog(@"Player: load!"); - NSString *albumId = call.arguments[@"albumId"]; - NSString *albumTitle = call.arguments[@"albumTitle"]; - NSString *name = call.arguments[@"name"]; - NSString *author = call.arguments[@"author"]; - NSString *url = call.arguments[@"url"]; - NSString *coverUrl = call.arguments[@"coverUrl"]; - NSString *cookie = call.arguments[@"cookie"]; - if (albumId == nil) - result(0); - if (name == nil) - result(0); - if (author == nil) - result(0); - if (url == nil) - result(0); - if (cookie == nil) - result(0); - if (call.arguments[@"isLocal"] == nil) - result(0); - if (call.arguments[@"volume"] == nil) - result(0); - if (call.arguments[@"position"] == nil) - result(0); - if (call.arguments[@"respectSilence"] == nil) - result(0); - if (coverUrl == nil) { - coverUrl = DEFAULT_COVER; + NSLog(@"Player: enqueue!"); + NSDictionary *batch = call.arguments; + NSArray *items = batch[@"batch"]; + BOOL autoPlay = [batch[@"autoPlay"] boolValue]; + NSString *cookie = batch[@"cookie"]; + + if (items == nil || [items count] == 0) { + result(@(NotOk)); + return; + } + + if (cookie != nil) { + // Atualizar o cookie se necessário + lastCookie = cookie; } - int isLocal = [call.arguments[@"isLocal"]intValue] ; + //TODO: Passar para cá isLocal +// int isLocal = [call.arguments[@"isLocal"]intValue] ; + int isLocal = false; float volume = (float)[call.arguments[@"volume"] doubleValue] ; - int milliseconds = call.arguments[@"position"] == [NSNull null] ? 0.0 : [call.arguments[@"position"] intValue] ; - bool respectSilence = [call.arguments[@"respectSilence"]boolValue] ; - CMTime time = CMTimeMakeWithSeconds(milliseconds / 1000,NSEC_PER_SEC); + //TODO: Passar para cá respectSilence +// bool respectSilence = [call.arguments[@"respectSilence"]boolValue] ; + bool respectSilence = false ; + + playlistItems = [[NSMutableArray alloc] init]; + for (NSDictionary *item in items) { + NSString *albumId = [self ensureStringValue:item[@"albumId"]]; + NSString *albumTitle = [self ensureStringValue:item[@"albumTitle"]]; + NSString *name = [self ensureStringValue:item[@"name"]]; + NSString *author = [self ensureStringValue:item[@"author"]]; + NSString *url = [self ensureStringValue:item[@"url"]]; + NSString *coverUrl = [self ensureStringValue:item[@"coverUrl"]]; + int milliseconds = call.arguments[@"position"] == [NSNull null] ? 0.0 : [call.arguments[@"position"] intValue] ; + CMTime time = CMTimeMakeWithSeconds(milliseconds / 1000,NSEC_PER_SEC); + PlaylistItem *currentItem = [[PlaylistItem alloc] initWithAlbumId:albumId albumName:albumTitle title:name artist:author url:url coverUrl:coverUrl]; + [playlistItems addObject:currentItem]; + } + + NSLog(@"Player: Enqueuing %lu items", (unsigned long)[playlistItems count]); + for (PlaylistItem *item in playlistItems) { + NSLog(@"Player: Item - Title: %@, Artist: %@, Album: %@, URL: %@", + item.title, item.artist, item.albumName, item.url); + } - currentItem = [[PlaylistItem alloc] initWithAlbumId:albumId albumName:albumTitle title:name artist:author url:url coverUrl:coverUrl]; - lastName = name; - lastAuthor = author; - lastUrl = url; - lastCoverUrl = coverUrl; - lastCookie = cookie; - lastVolume = volume; - lastTime = time; - lastRespectSilence = respectSilence; - int ret = [self load:playerId name:name author:author url:url coverUrl:coverUrl cookie:cookie isLocal:isLocal volume:volume time:time isNotification:respectSilence]; - result(@(ret)); + + +// int ret = [self load:playerId mediaItems:playlistItems cookie:cookie isLocal:isLocal volume:volume isNotification:respectSilence]; + + + result(@(Ok)); }, @"play": ^{ @@ -777,12 +792,21 @@ -(void) configurePlayer:(NSString *)playerId url:(NSString *)url { } } +-(NSString *)ensureStringValue:(id)value { + if ([value isKindOfClass:[NSString class]]) { + return value; + } else if ([value isKindOfClass:[NSNumber class]]) { + return [(NSNumber *)value stringValue]; + } + return @""; +} + -(void) initAVPlayer:(NSString *)playerId playerItem:(AVPlayerItem *)playerItem url:(NSString *)url onReady:(VoidCallback) onReady { NSMutableDictionary * playerInfo = players[_playerId]; - __block AVPlayer *player = nil; + __block AVQueuePlayer *player = nil; dispatch_async (playerQueue, ^{ - player = [[ AVPlayer alloc ] init]; + player = [[ AVQueuePlayer alloc ] init]; [self configurePlayer: playerId url:url]; player.allowsExternalPlayback = FALSE; [player replaceCurrentItemWithPlayerItem:playerItem]; @@ -1496,80 +1520,84 @@ -(int) configureAudioSession: (NSString *) playerId } -(int) load: (NSString*) playerId - name: (NSString*) name - author: (NSString*) author - url: (NSString*) url - coverUrl: (NSString*) coverUrl +playlistItems:(NSMutableArray*) playlistItems cookie: (NSString *) cookie isLocal: (int) isLocal volume: (float) volume - time: (CMTime) time isNotification: (bool) respectSilence { - loadOnly = true; - if ([self ensureConnected:playerId isLocal:isLocal] == -1) { - return -1; - } - - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - if (player.rate != 0) { - [player pause]; - } - - if (!@available(iOS 11,*)) { - url = [url stringByReplacingOccurrencesOfString:@".m3u8" - withString:@".mp3"]; - url = [url stringByReplacingOccurrencesOfString:@"stream/" - withString:@""]; - } - latestUrl = url; - latestIsLocal = isLocal; - latestCookie = cookie; - latestPlayerId = playerId; - latestOnReady = ^(NSString * playerId) { - NSLog(@"Player: Inside OnReady"); - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - [ player setVolume:volume ]; - [ player seekToTime:time ]; - if(!loadOnly){ - [ player play]; - } - }; - - NSLog(@"Player: Volume: %f", volume); - - [self configureRemoteCommandCenter]; - if ([self configureAudioSession:playerId] != Ok) { - if (!loadOnly) { - [_channel_player invokeMethod:@"audio.onError" arguments:@{@"playerId": _playerId, @"errorType": @(PLAYER_ERROR_FAILED)}]; - return NotOk; - } - } - - if (name == nil) { - name = @"unknown"; - } - - if (author == nil) { - author = @"unknown"; - } - - if (coverUrl == nil) { - coverUrl = DEFAULT_COVER; - } - - NSLog(@"Player: [SET_CURRENT_ITEM LOG] playerId=%@ name=%@ author=%@ url=%@ coverUrl=%@", playerId, name, author, url, coverUrl); - [self setCurrentItem:playerId name:name author:author url:url coverUrl:coverUrl]; - - - [self setUrl:url - isLocal:isLocal - cookie:cookie - playerId:playerId - shallPlay: false - onReady:latestOnReady]; +// loadOnly = true; +// if ([self ensureConnected:playerId isLocal:isLocal] == -1) { +// return -1; +// } +// +// NSMutableDictionary * playerInfo = players[playerId]; +// AVPlayer *player = playerInfo[@"player"]; +// if (player.rate != 0) { +// [player pause]; +// } +// +// +// +// for (PlaylistItem *item in playlistItems) { +// if (!@available(iOS 11,*)) { +// item.url = [item.url stringByReplacingOccurrencesOfString:@".m3u8" +// withString:@".mp3"]; +// item.url = [item.url stringByReplacingOccurrencesOfString:@"stream/" +// withString:@""]; +// } +// +// +// latestUrl = item.url; +// latestIsLocal = isLocal; +// latestCookie = cookie; +// latestPlayerId = playerId; +// latestOnReady = ^(NSString * playerId) { +// NSLog(@"Player: Inside OnReady"); +// NSMutableDictionary * playerInfo = players[playerId]; +// AVPlayer *player = playerInfo[@"player"]; +// [ player setVolume:volume ]; +// [ player seekToTime:item.duration ]; +// if(!loadOnly){ +// [ player play]; +// } +// }; +// +// NSLog(@"Player: Volume: %f", volume); +// +// [self configureRemoteCommandCenter]; +// if ([self configureAudioSession:playerId] != Ok) { +// if (!loadOnly) { +// [_channel_player invokeMethod:@"audio.onError" arguments:@{@"playerId": _playerId, @"errorType": @(PLAYER_ERROR_FAILED)}]; +// return NotOk; +// } +// } +// +// if (item.name == nil) { +// item.name = @"unknown"; +// } +// +// if (item.author == nil) { +// item.author = @"unknown"; +// } +// +// if (item.coverUrl == nil) { +// item.coverUrl = DEFAULT_COVER; +// } +// +// NSLog(@"Player: [SET_CURRENT_ITEM LOG] playerId=%@ name=%@ author=%@ url=%@ coverUrl=%@", playerId, name, author, url, coverUrl); +// [self setCurrentItem:playerId name:name author:author url:url coverUrl:coverUrl]; +// +// +// [self setUrl:url +// isLocal:isLocal +// cookie:cookie +// playerId:playerId +// shallPlay: false +// onReady:latestOnReady]; +// } + + return Ok; } diff --git a/packages/player/ios/Classes/PlaylistItem.swift b/packages/player/ios/Classes/PlaylistItem.swift index 50b68351..80d970c0 100644 --- a/packages/player/ios/Classes/PlaylistItem.swift +++ b/packages/player/ios/Classes/PlaylistItem.swift @@ -1,21 +1,21 @@ import AVFoundation @objc public class PlaylistItem : NSObject { - @objc public let error: Error? + @objc public var error: Error? - @objc public let albumId: String + @objc public var albumId: String - @objc public let albumName: String + @objc public var albumName: String - @objc public let title: String + @objc public var title: String - @objc public let artist: String + @objc public var artist: String - @objc public let url: String? + @objc public var url: String? - @objc public let coverUrl: String? + @objc public var coverUrl: String? - @objc public let duration: CMTime + @objc public var duration: CMTime @objc public init(albumId: String, albumName: String, title: String, artist: String, url: String, coverUrl: String) { self.albumId = albumId From 577b51d6016c42a6d67b2ae97bfc89d9d16817f9 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 26 Sep 2024 18:51:31 -0300 Subject: [PATCH 45/70] playing and notifying --- .../example/ios/Flutter/Flutter.podspec | 18 +++ .../ios/Runner.xcodeproj/project.pbxproj | 10 +- packages/player/example/lib/sm_player.dart | 28 +++- packages/player/ios/Classes/Consts.swift | 62 +++++++++ packages/player/ios/Classes/Media.swift | 32 +++++ .../ios/Classes/MethodChannelManager.swift | 62 +++++++++ .../Classes/MethodChannelManagerArgs.swift | 67 ++++++++++ packages/player/ios/Classes/NSString+MD5.h | 5 - packages/player/ios/Classes/NSString+MD5.m | 23 ---- .../player/ios/Classes/NowPlayingCenter.swift | 2 +- packages/player/ios/Classes/PlayerPlugin.h | 4 - .../player/ios/Classes/PlayerPlugin.swift | 106 ++++++++++++++++ packages/player/ios/Classes/PlayerState.swift | 23 ++++ packages/player/ios/Classes/SMPlayer.swift | 91 +++++++++++++ .../ios/Classes/SMPlayerListeners.swift | 120 ++++++++++++++++++ .../player/ios/Classes/SMPlayerNotifier.swift | 42 ++++++ 16 files changed, 654 insertions(+), 41 deletions(-) create mode 100644 packages/player/example/ios/Flutter/Flutter.podspec create mode 100644 packages/player/ios/Classes/Consts.swift create mode 100644 packages/player/ios/Classes/Media.swift create mode 100644 packages/player/ios/Classes/MethodChannelManager.swift create mode 100644 packages/player/ios/Classes/MethodChannelManagerArgs.swift delete mode 100644 packages/player/ios/Classes/NSString+MD5.h delete mode 100644 packages/player/ios/Classes/NSString+MD5.m delete mode 100644 packages/player/ios/Classes/PlayerPlugin.h create mode 100644 packages/player/ios/Classes/PlayerPlugin.swift create mode 100644 packages/player/ios/Classes/PlayerState.swift create mode 100644 packages/player/ios/Classes/SMPlayer.swift create mode 100644 packages/player/ios/Classes/SMPlayerListeners.swift create mode 100644 packages/player/ios/Classes/SMPlayerNotifier.swift diff --git a/packages/player/example/ios/Flutter/Flutter.podspec b/packages/player/example/ios/Flutter/Flutter.podspec new file mode 100644 index 00000000..98e16339 --- /dev/null +++ b/packages/player/example/ios/Flutter/Flutter.podspec @@ -0,0 +1,18 @@ +# +# This podspec is NOT to be published. It is only used as a local source! +# This is a generated file; do not edit or check into version control. +# + +Pod::Spec.new do |s| + s.name = 'Flutter' + s.version = '1.0.0' + s.summary = 'A UI toolkit for beautiful and fast apps.' + s.homepage = 'https://flutter.dev' + s.license = { :type => 'BSD' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } + s.ios.deployment_target = '12.0' + # Framework linking is handled by Flutter tooling, not CocoaPods. + # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. + s.vendored_frameworks = 'path/to/nothing' +end diff --git a/packages/player/example/ios/Runner.xcodeproj/project.pbxproj b/packages/player/example/ios/Runner.xcodeproj/project.pbxproj index 248592ec..2968f167 100644 --- a/packages/player/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/player/example/ios/Runner.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - C4606F2BB22E4BE921F7A613 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E2EC85FF3066B718322F54ED /* libPods-Runner.a */; }; + F32C5B43BBA2FCC13F18A5A7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1EF38F8581E032C5190CA4C3 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -33,6 +33,7 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1EF38F8581E032C5190CA4C3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 6CC4F166AE3E8014E3F4BA5B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 722582E8419D6ADA9D16406F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; @@ -48,7 +49,6 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9E368E561B4EA20972BECE95 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - E2EC85FF3066B718322F54ED /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,7 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C4606F2BB22E4BE921F7A613 /* libPods-Runner.a in Frameworks */, + F32C5B43BBA2FCC13F18A5A7 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -130,7 +130,7 @@ DF2798DF4DC35E5EB02BC811 /* Frameworks */ = { isa = PBXGroup; children = ( - E2EC85FF3066B718322F54ED /* libPods-Runner.a */, + 1EF38F8581E032C5190CA4C3 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -232,12 +232,14 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/ComScore/SCORBundle.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_foundation/path_provider_foundation_privacy.bundle", "${PODS_ROOT}/../.symlinks/plugins/smplayer/ios/Classes/sm_cd_cover.png", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SCORBundle.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/path_provider_foundation_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/sm_cd_cover.png", diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 8da8ccd4..fc76fca1 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:smaws/aws.dart'; import 'package:smplayer/player.dart'; @@ -108,6 +110,25 @@ class _SMPlayerState extends State { playlistId: 0, ); + var media3 = Media( + id: 1, + albumTitle: "Album unsigned", + albumId: 1, + name: "Track unsigned", + url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", + coverUrl: "https://picsum.photos/500/500", + bigCoverUrl: "https://picsum.photos/500/500", + author: "Xand Avião", + fallbackUrl: + "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", + isLocal: false, + isVerified: true, + shareUrl: "", + isSpot: false, + ownerId: 0, + playlistId: 0, + ); + var media2 = Media( id: 2, albumTitle: "Album", @@ -127,8 +148,7 @@ class _SMPlayerState extends State { playlistId: 0, ); - player.enqueue(media: media1); - player.enqueue(media: media2); + player.enqueueAll([media1, media2, media3], autoPlay: true); if (!mounted) return; @@ -222,10 +242,10 @@ class _SMPlayerState extends State { void shuffleOrUnshuffle() { setState(() { if (_shuffled) { - _player.unshuffle(); + _player.toggleShuffle(); _shuffled = false; } else { - _player.shuffle(); + _player.toggleShuffle(); _shuffled = true; } }); diff --git a/packages/player/ios/Classes/Consts.swift b/packages/player/ios/Classes/Consts.swift new file mode 100644 index 00000000..fa42c666 --- /dev/null +++ b/packages/player/ios/Classes/Consts.swift @@ -0,0 +1,62 @@ +// +// Consts.swift +// AFNetworking +// +// Created by Lucas Tonussi on 25/09/24. +// +import Foundation +//CHANNEL METHODS +let INITIALIZE_METHOD = "initialize" +let SEEK_METHOD = "seek" +let PREVIOUS_METHOD = "previous" +let STOP_METHOD = "stop" +let RESUME_METHOD = "resume" +let PLAY_METHOD = "play" +let PLAY_EXTERNAL_METHOD = "play_external" +let PAUSE_METHOD = "pause" +let PAUSE_EXTERNAL_METHOD = "pause_external" +let NEXT_METHOD = "next" +let ENQUEUE_METHOD = "enqueue" +let CLEAR_QUEUE = "clear_queue" +let RETURN_QUEUE = "return_queue" +let REORDER_QUEUE = "reorder_queue" +let TOGGLE_FAVORITE = "toggle_favorite" +let TOGGLE_SHUFFLE = "toggle_shuffle" +let REPEAT_MODE = "repeat_mode" +let DELETE_ITEM_IN_QUEUE = "delete_item_in_queue" +let PLAY_ITEM_QUEUE_FROM_INDEX = "play_item_queue_from_index" +let GET_EXTRAS = "get_extras" +let UPDATE_FAVORITE_ICON = "update_favorite_icon" +let CONTINUE_TO_PLAY = "continue_to_play" +let SHOW_LOG_IN_RELEASE = "show_log_in_release" +let TOGGLE_EXTERNAL_PLAYER = "toggle_external_player" +let UPDATE_ADS_TAG = "update_ads_tag" +let UPDATE_SHOULD_SHOW_ADS = "update_should_show_ads" +//ARGS CHANNEL +let PLAYER_ID_ARGS = "player_id_args" +let ADS_EVENT_ARGS = "ads_event_args" +let ADS_EVENT_IS_AUDIO = "ads_event_is_audio" +let ADS_CODE_ERROR_ARGS = "ads_code_error_args" +let ADS_MESSAGE_ERROR_ARGS = "ads_message_error_args" +let POSITION_ARGS = "position" +let SHUFFLE_ARGS = "shuffle_args" +let CURRENT_MEDIA_INDEX_ARGS = "current_media_index_args" +let DURATION_ARGS = "duration" +let STATE_ARGS = "state" +let ITS_AD_TIME = "its_ad_time" +let ERROR_ARGS = "error_args" +let QUEUE_ARGS = "queue_args" +let EXTRAS_ARGS = "extras_args" +let IS_FAVORITE_ARGS = "is_favorite_args" +let REPEAT_MODE_ARGS = "repeat_mode_args" +let EVENT_ARGS = "event_args" + +//EVENTS CHANNEL +let POSITION_CHANGE = "position_change" +let REPEAT_MODE_CHANGED = "repeat_mode_changed" +let SHUFFLE_CHANGED = "shuffle_changed" +let ON_ADS = "on_ads" +let NOTIFY_PLAYER_EXTERNAL = "notify_player_external" +let STATE_CHANGE = "state_change" +let CURRENT_QUEUE = "current_queue" +let FAVORITE_UPDATE = "favorite_update" diff --git a/packages/player/ios/Classes/Media.swift b/packages/player/ios/Classes/Media.swift new file mode 100644 index 00000000..622b643b --- /dev/null +++ b/packages/player/ios/Classes/Media.swift @@ -0,0 +1,32 @@ +class Media { + let id: Int + let name: String + let albumId: Int + let albumTitle: String + let author: String + var url: String + var isLocal: Bool + let localPath: String? + let coverUrl: String + let bigCoverUrl: String + let isVerified: Bool + let playlistId: Int? + var fallbackUrl: String? + + // Inicializador para a classe + init(id: Int, name: String, albumId: Int, albumTitle: String, author: String, url: String, isLocal: Bool, localPath: String?, coverUrl: String, bigCoverUrl: String, isVerified: Bool, playlistId: Int?, fallbackUrl: String?) { + self.id = id + self.name = name + self.albumId = albumId + self.albumTitle = albumTitle + self.author = author + self.url = url + self.isLocal = isLocal + self.localPath = localPath + self.coverUrl = coverUrl + self.bigCoverUrl = bigCoverUrl + self.isVerified = isVerified + self.playlistId = playlistId + self.fallbackUrl = fallbackUrl + } +} diff --git a/packages/player/ios/Classes/MethodChannelManager.swift b/packages/player/ios/Classes/MethodChannelManager.swift new file mode 100644 index 00000000..24310e8b --- /dev/null +++ b/packages/player/ios/Classes/MethodChannelManager.swift @@ -0,0 +1,62 @@ +// +// MethodChannelManager.swift +// smplayer +// +// Created by Lucas Tonussi on 26/09/24. +// + + + +import Foundation +import Flutter.FlutterPlugin + + + +public class MethodChannelManager:NSObject{ + let channel: FlutterMethodChannel? + init(channel: FlutterMethodChannel?) { + self.channel = channel + } + var tag = "MethodChannelManager" +// let methodChannel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger) + + + func notifyPositionChange( + position: Double, + duration: Double) { + let args = MethodChannelManagerArgsBuilder() + .playerId(id:"TAG_ONEPLAYER") + .position(position:Int(position)*1000) + .duration(duration:Int(duration)*1000) +// .currentMediaIndex(currentMediaIndex:currentMediaIndex) + .build() + print("notifyPositionChange no tempo: \(position) segundos | duration: \(duration)") + channel?.invokeMethod("audio.onCurrentPosition", arguments: args) + } + + + func notifyPlayerStateChange(state: PlayerState) { + let args = MethodChannelManagerArgsBuilder() + .playerId(id:"TAG_ONEPLAYER") + .state(state:state) + .build() + channel?.invokeMethod("state.change", arguments: args) + } +// +// func repeatModeChanged(repeatMode: Int) { +// let args = MethodChannelManagerArgsBuilder() +// .event(event: REPEAT_MODE_CHANGED) +// .repeatMode(repeatMode: repeatMode) +// .build() +// eventSink?(args) +// } +// +// func shuffleChanged(shuffleIsActive: Bool) { +// let args = MethodChannelManagerArgsBuilder() +// .event(event: SHUFFLE_CHANGED) +// .shuffleIsActive(shuffleIsActive: shuffleIsActive) +// .build() +// eventSink?(args) +// } +} + diff --git a/packages/player/ios/Classes/MethodChannelManagerArgs.swift b/packages/player/ios/Classes/MethodChannelManagerArgs.swift new file mode 100644 index 00000000..5cb88fcd --- /dev/null +++ b/packages/player/ios/Classes/MethodChannelManagerArgs.swift @@ -0,0 +1,67 @@ +// +// MethodChannelManagerArgsBuilder.swift +// oneplayer_ios +// +// Created by Lucas Tonussi on 04/10/22. +// + +import Foundation +class MethodChannelManagerArgsBuilder{ + var tag = "MethodChannelManagerArgsBuilder" + var args : Dictionary = [:] + + open func build() -> Dictionary{ + return args + } + + open func position(position: Int) -> MethodChannelManagerArgsBuilder{ + args[POSITION_ARGS] = position + return self + } + + func shuffleIsActive(shuffleIsActive: Bool) -> MethodChannelManagerArgsBuilder { + args[SHUFFLE_ARGS] = shuffleIsActive + return self + } + + open func duration(duration: Int) -> MethodChannelManagerArgsBuilder { + args[DURATION_ARGS] = duration + return self + } + + open func playerId(id: String) -> MethodChannelManagerArgsBuilder { + args[PLAYER_ID_ARGS] = id + return self + } + + open func currentMediaIndex(currentMediaIndex: Int) -> MethodChannelManagerArgsBuilder { + args[CURRENT_MEDIA_INDEX_ARGS] = currentMediaIndex + return self + } + + open func state(state: PlayerState) -> MethodChannelManagerArgsBuilder { + args[STATE_ARGS] = state.rawValue + return self + } + +// open func queue(queue: Array) -> MethodChannelManagerArgsBuilder { +// let queueJson = JsonUtil.toJson(queue) as? String +// //TODO: Melhorar o tratamento do null +// args[QUEUE_ARGS] = queueJson!.replacingOccurrences(of: "{}", with: "null") +// return self +// } + + open func error(error: String?) -> MethodChannelManagerArgsBuilder { + if(error != nil){ + args[ERROR_ARGS] = error + } + return self + } + + + open func repeatMode(repeatMode: Int) -> MethodChannelManagerArgsBuilder { + args[REPEAT_MODE_ARGS] = repeatMode + return self + } + +} diff --git a/packages/player/ios/Classes/NSString+MD5.h b/packages/player/ios/Classes/NSString+MD5.h deleted file mode 100644 index da017a14..00000000 --- a/packages/player/ios/Classes/NSString+MD5.h +++ /dev/null @@ -1,5 +0,0 @@ -@interface NSString (MD5) - -- (NSString *)MD5; - -@end diff --git a/packages/player/ios/Classes/NSString+MD5.m b/packages/player/ios/Classes/NSString+MD5.m deleted file mode 100644 index 43abd214..00000000 --- a/packages/player/ios/Classes/NSString+MD5.m +++ /dev/null @@ -1,23 +0,0 @@ -#import - -@implementation NSString(MD5) - -- (NSString*)MD5 -{ - // Create pointer to the string as UTF8 - const char *ptr = [self UTF8String]; - - // Create byte array of unsigned chars - unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH]; - - // Create 16 byte MD5 hash value, store in buffer - CC_MD5(ptr, strlen(ptr), md5Buffer); - - // Convert MD5 value in the buffer to NSString of hex values - NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2]; - for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) - [output appendFormat:@"%02x",md5Buffer[i]]; - - return output; -} -@end diff --git a/packages/player/ios/Classes/NowPlayingCenter.swift b/packages/player/ios/Classes/NowPlayingCenter.swift index 078dae2d..990b0445 100644 --- a/packages/player/ios/Classes/NowPlayingCenter.swift +++ b/packages/player/ios/Classes/NowPlayingCenter.swift @@ -1,6 +1,6 @@ import MediaPlayer -@objc public class NowPlayingCenter : NSObject { +public class NowPlayingCenter : NSObject { @objc static public func set(item: PlaylistItem?) { DispatchQueue.main.async { diff --git a/packages/player/ios/Classes/PlayerPlugin.h b/packages/player/ios/Classes/PlayerPlugin.h deleted file mode 100644 index f53afcd0..00000000 --- a/packages/player/ios/Classes/PlayerPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface PlayerPlugin : NSObject -@end diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift new file mode 100644 index 00000000..fe72da55 --- /dev/null +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -0,0 +1,106 @@ +import Flutter +import UIKit +import AVFoundation +import Foundation +//import Alamofire + +let TAG = "SMPlayerIos" +let CHANNEL = "suamusica.com.br/player" +let CHANNEL_NOTIFICATION = "One_Player_Notification" +let NOTIFICATION_ID = 0xb339 +//let MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8 +//let AUDIO_MPEG = MimeTypes.BASE_TYPE_AUDIO +var registrarAds: FlutterPluginRegistrar? = nil +private let tag = TAG + +private var smPlayer: SMPlayer? = nil + + + + +public class PlayerPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: registrar.messenger()) + let instance = PlayerPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + + registrarAds = registrar + smPlayer = SMPlayer(methodChannelManager: MethodChannelManager(channel: channel)) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + print("call.method: \(call.method)") + switch call.method { + case "enqueue": + if let batch = call.arguments as? [String: Any], + let listMedia = batch["batch"] as? [[String: Any]]{ + let autoPlay = batch["autoPlay"] as? Bool ?? false + let cookie = batch["cookie"] as? String ?? "" + if convertToMedia(mediaArray: listMedia) != nil { + smPlayer?.enqueue(medias: convertToMedia(mediaArray: listMedia)!, autoPlay: autoPlay,cookie: cookie) + } + } + result(NSNumber(value: true)) + case "next": + smPlayer?.next() + result(NSNumber(value: 1)) + case "play": + smPlayer?.play() + result(NSNumber(value: 1)) + case "pause": + smPlayer?.pause() + result(NSNumber(value: 1)) + case "remove_all": + result(NSNumber(value: true)) + default: + result(NSNumber(value: false)) + } + } + + func convertToMedia(mediaArray: [[String: Any]]) -> [Media]? { + var mediaList: [Media] = [] + + for mediaDict in mediaArray { + + var id = mediaDict["id"] // Certificar que 'id' é um número + let name = mediaDict["name"] + let albumId = mediaDict["albumId"] + let albumTitle = mediaDict["albumTitle"] + let author = mediaDict["author"] + let url = mediaDict["url"] + let isLocal = mediaDict["isLocal"] + let coverUrl = mediaDict["coverUrl"] + let bigCoverUrl = mediaDict["bigCoverUrl"] + let isVerifiedString = mediaDict["isVerified"] + let isVerified = isVerifiedString + + + + // Atribuição opcional + let localPath = mediaDict["localPath"] + let playlistId = mediaDict["playlistId"] + let fallbackUrl = mediaDict["fallbackUrl"] + + // Criar o objeto Media e adicionar à lista + let media = Media( + id: id as! Int, + name: name as! String, + albumId: albumId as! Int, + albumTitle: albumTitle as! String, + author: author as! String, + url: url as! String, + isLocal: isLocal as? Bool ?? false, + localPath: localPath as? String ?? "", + coverUrl: coverUrl as? String ?? "", + bigCoverUrl: bigCoverUrl as? String ?? "", + isVerified: isVerified as? Bool ?? false, + playlistId: playlistId as? Int ?? 0, + fallbackUrl: fallbackUrl as? String ?? "" + ) + mediaList.append(media) + } + + return mediaList + } + +} diff --git a/packages/player/ios/Classes/PlayerState.swift b/packages/player/ios/Classes/PlayerState.swift new file mode 100644 index 00000000..9bf7c195 --- /dev/null +++ b/packages/player/ios/Classes/PlayerState.swift @@ -0,0 +1,23 @@ +// +// PlayerState.swift +// smplayer +// +// Created by Lucas Tonussi on 26/09/24. +// + +import Foundation + +enum PlayerState: Int { + case idle = 0 + case buffering + case playing + case paused + case stopped + case completed + case error + case seekEnd + case bufferEmpty + case itemTransition + case stateEnded + case stateReady +} diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift new file mode 100644 index 00000000..f40d41a5 --- /dev/null +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -0,0 +1,91 @@ +import Foundation +import AVFoundation +import MediaPlayer + +public class SMPlayer : NSObject { + var methodChannelManager: MethodChannelManager? + private var smPlayer: AVQueuePlayer + private var playerItem: AVPlayerItem? + private var queue : [AVPlayerItem] = [] + + + init(methodChannelManager: MethodChannelManager?) { + smPlayer = AVQueuePlayer() + super.init() + self.methodChannelManager = methodChannelManager + let listeners = SMPlayerListeners(playerItem: playerItem,smPlayer:smPlayer,methodChannelManager:methodChannelManager) + listeners.addObservers() + } + + private func setupNowPlaying() { + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.addTarget { [weak self] event in + self?.smPlayer.play() + return .success + } + commandCenter.pauseCommand.addTarget { [weak self] event in + self?.pause() + return .success + } + } + + func play(url: String) { + guard let videoURL = URL(string: url) else { return } + playerItem = AVPlayerItem(url: videoURL) + smPlayer.replaceCurrentItem(with: playerItem) + updateNowPlayingInfo() + smPlayer.play() + } + + func pause() { + smPlayer.pause() + } + + func stop() { + smPlayer.pause() + smPlayer.replaceCurrentItem(with: nil) + } + + func enqueue(medias: [Media], autoPlay: Bool, cookie: String) { + for media in medias { + guard let url = URL(string: media.url) else { continue } + queue.append(AVPlayerItem(url: url)) + let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": [ "Cookie": cookie]] + let playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) + smPlayer.insert(playerItem, after: nil) + } + + if(autoPlay){ + smPlayer.play() + } + } + + func next(){ + smPlayer.advanceToNextItem() + } + + func removeAll(){ + smPlayer.removeAllItems() + queue.removeAll() + smPlayer.pause() + smPlayer.seek(to: .zero) + } + + func play(){ + smPlayer.play() + } + + private func updateNowPlayingInfo() { +// guard let playerItem = playerItem else { return } +// let artwork = MPMediaItemArtwork(image: UIImage(named: "queue.first.co)")) +// let nowPlayingInfo: [String: Any] = [ +// MPMediaItemPropertyTitle: "Title", +// MPMediaItemPropertyArtist: "Artist", +// MPMediaItemPropertyArtwork: artwork, +// MPMediaItemPropertyPlaybackDuration: playerItem.asset.duration.seconds, +// MPNowPlayingInfoPropertyElapsedPlaybackTime: smPlayer.currentTime().seconds, +// MPNowPlayingInfoPropertyPlaybackRate: smPlayer.rate +// ] +// MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } +} diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift new file mode 100644 index 00000000..e1eaa711 --- /dev/null +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -0,0 +1,120 @@ +// +// SMPlayerListeners.swift +// smplayer +// +// Created by Lucas Tonussi on 26/09/24. +// + +import Foundation +import AVFoundation + +public class SMPlayerListeners : NSObject { + var playerItem:AVPlayerItem? + let smPlayer:AVQueuePlayer + let methodChannelManager: MethodChannelManager? + + init(playerItem: AVPlayerItem?, smPlayer: AVQueuePlayer,methodChannelManager: MethodChannelManager?) { + self.playerItem = playerItem + self.smPlayer = smPlayer + self.methodChannelManager = methodChannelManager + } + + private var mediaChange: NSKeyValueObservation? + private var statusChange: NSKeyValueObservation? + private var loading: NSKeyValueObservation? + private var loaded: NSKeyValueObservation? + private var error: NSKeyValueObservation? + private var notPlayingReason: NSKeyValueObservation? + private var playback: NSKeyValueObservation? + + func addObservers() { + let interval = CMTime(seconds: 0.5, + preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [self] time in + let position : Float64 = CMTimeGetSeconds(smPlayer.currentTime()); + if(smPlayer.currentItem != nil){ + let duration : Float64 = CMTimeGetSeconds(smPlayer.currentItem!.duration); + if(position < duration){ + methodChannelManager?.notifyPositionChange(position: position, duration: duration) + } + } + } + + mediaChange = smPlayer.observe(\.currentItem, options: [.new,.old]) { [self] + (player, item) in + if(item.oldValue! != nil && (item.newValue != item.oldValue)){ +// OnePlayerSingleton.i.currentInterval += 1 + } + if(item.newValue != item.oldValue){ + print("onMediaChanged") + } + } + + statusChange = smPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { + (playerItem, change) in + if playerItem.status == .readyToPlay { + print("readyToPlay") + } else if playerItem.status == .failed { + print("failed") + } + }) + + loading = smPlayer.currentItem?.observe(\.isPlaybackBufferEmpty, options: [.new,.old]) { [self] + (new, old) in + print("observer - loading") + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + } + + loaded = smPlayer.currentItem?.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { + (player, _) in +// OnePlayerSingleton.i.log(tag: tag, message: "observer - loaded \(player)") + print("observer - loaded") + } + + + notPlayingReason = smPlayer.observe(\.reasonForWaitingToPlay, options: [.new], changeHandler: { [self] + (playerItem, change) in + switch (smPlayer.reasonForWaitingToPlay) { + case AVPlayer.WaitingReason.evaluatingBufferingRate: + print("evaluatingBufferingRate") + case AVPlayer.WaitingReason.toMinimizeStalls: + print("toMinimizeStalls") + case AVPlayer.WaitingReason.noItemToPlay: + print("noItemToPlay") + default: + print("default") + } + }) + + playback = smPlayer.observe(\.timeControlStatus, options: [.new, .old], changeHandler: { [self] + (playerItem, change) in + switch (playerItem.timeControlStatus) { + case AVPlayer.TimeControlStatus.paused: + print("observer - paused") +// onePlayerManager?.onStateChange(state: OnePlayerState.PAUSED) + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.paused) + break + case AVPlayer.TimeControlStatus.playing: + print("observer - playing") + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.playing) + break + case AVPlayer.TimeControlStatus.waitingToPlayAtSpecifiedRate: + print("observer - waitingToPlayAtSpecifiedRate") + break + default: + print("observer - default: \(AVPlayer.TimeControlStatus.self)") + break + } + }) + + smPlayer.addObserver(self, forKeyPath: "error", options: [.old, .new], context: nil) + } + public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "error", let change = change, let error = change[.newKey] as? Error { + if (error as NSError).code == AVError.contentIsNotAuthorized.rawValue { + print("Erro HTTP: contentIsNotAuthorized") + } + } + } + +} diff --git a/packages/player/ios/Classes/SMPlayerNotifier.swift b/packages/player/ios/Classes/SMPlayerNotifier.swift new file mode 100644 index 00000000..c0b381fa --- /dev/null +++ b/packages/player/ios/Classes/SMPlayerNotifier.swift @@ -0,0 +1,42 @@ +// +// SMPlayerNotifier.swift +// smplayer +// +// Created by Lucas Tonussi on 26/09/24. +// +private let tag = "SMPlayerNotifier" + +import Foundation +public class SMPlayerNotifier : NSObject{ + + let methodChannelManager: MethodChannelManager? + init(methodChannelManager: MethodChannelManager?) { + self.methodChannelManager = methodChannelManager + } + + func notifyPositionChange(position: Double, duration: Double, currentMediaIndex: Int) { + if (duration >= 0 && position >= 0) { + methodChannelManager?.notifyPositionChange(position:position, duration:duration) + } + } + +// func notifyPlayerStateChange( state: OnePlayerState, +// error: String? = nil, +// currentMediaIndex: Int, +// itsAdTime: Bool?){ +// methodChannelManager.notifyPlayerStateChange(state: state, currentMediaIndex: currentMediaIndex, itsAdTime: itsAdTime) +// } + +// func repeatModeChanged(repeatMode:Int){ +// methodChannelManager?.repeatModeChanged(repeatMode: repeatMode) +// } +// +// func onAds(status: String,code: String?, message: String?, isAudioAd: Bool = false){ +// methodChannelManager>.onAds(status: status, code: code, message: message, isAudioAd: isAudioAd) +// } +// +// func shuffleChanged(shuffleIsActive: Bool){ +// methodChannelManager?.shuffleChanged(shuffleIsActive: shuffleIsActive) +// } + +} From fcd81c6fff6de3bb0dd63b6b6b64980a93700d6c Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 27 Sep 2024 17:21:11 -0300 Subject: [PATCH 46/70] now playing ok --- .../ios/Classes/AudioSessionManager.swift | 98 +++++++------- packages/player/ios/Classes/Consts.swift | 3 +- .../ios/Classes/MethodChannelManager.swift | 10 +- .../Classes/MethodChannelManagerArgs.swift | 5 + .../player/ios/Classes/PlayerPlugin.swift | 42 +++--- .../player/ios/Classes/PlaylistItem.swift | 13 +- packages/player/ios/Classes/SMPlayer.swift | 122 +++++++++++++----- .../ios/Classes/SMPlayerListeners.swift | 67 +++++----- 8 files changed, 227 insertions(+), 133 deletions(-) diff --git a/packages/player/ios/Classes/AudioSessionManager.swift b/packages/player/ios/Classes/AudioSessionManager.swift index e9d31523..214ca6bc 100644 --- a/packages/player/ios/Classes/AudioSessionManager.swift +++ b/packages/player/ios/Classes/AudioSessionManager.swift @@ -2,54 +2,54 @@ import Foundation import MediaPlayer @objc public class AudioSessionManager: NSObject { - private static var _isActive : Bool = false - - @objc public static func isActive() -> Bool { - return _isActive; - } - - @objc public static func activeSession() -> Bool { - let audioSession = AVAudioSession.sharedInstance() - do { - if #available(iOS 11, *) { + private static var _isActive : Bool = false + + @objc public static func isActive() -> Bool { + return _isActive; + } + + @objc public static func activeSession() -> Bool { + let audioSession = AVAudioSession.sharedInstance() + do { + if #available(iOS 11, *) { try audioSession.setCategory(.playback, mode: .default, policy: .longFormAudio) - } else if #available(iOS 10, *) { - let audioSessionCategory: AVAudioSession.CategoryOptions = [.allowAirPlay, .allowBluetoothA2DP] - try audioSession.setCategory(.playback, mode: .default, options: audioSessionCategory) - } else { - // Workaround until https://forums.swift.org/t/using-methods-marked-unavailable-in-swift-4-2/14949 isn't fixed - audioSession.perform(NSSelectorFromString("setCategory:error:"), with: AVAudioSession.Category.playback) - } - } catch let error as NSError { - print("Player: Failed to set Audio Category \(error.localizedDescription)") - return false - } - - - do { - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - print("Player: Audio Session is Active - notifyOthersOnDeactivation") - } catch let error as NSError { - print("Player: Failed to activate Audio Session \(error.localizedDescription)") - return false - } - _isActive = true; - return true - } - - @objc public static func inactivateSession() -> Bool { - if !_isActive { - return true; - } - - do { - try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) - print("Player: Audio Session is Inactive - notifyOthersOnDeactivation") - } catch let error as NSError { - print("Player: Failed to activate Audio Session \(error.localizedDescription)") - return false - } - _isActive = false; - return true - } + } else if #available(iOS 10, *) { + let audioSessionCategory: AVAudioSession.CategoryOptions = [.allowAirPlay, .allowBluetoothA2DP] + try audioSession.setCategory(.playback, mode: .default, options: audioSessionCategory) + } else { + // Workaround until https://forums.swift.org/t/using-methods-marked-unavailable-in-swift-4-2/14949 isn't fixed + audioSession.perform(NSSelectorFromString("setCategory:error:"), with: AVAudioSession.Category.playback) + } + } catch let error as NSError { + print("Player: Failed to set Audio Category \(error.localizedDescription)") + return false + } + + + do { + try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + print("Player: Audio Session is Active - notifyOthersOnDeactivation") + } catch let error as NSError { + print("Player: Failed to activate Audio Session \(error.localizedDescription)") + return false + } + _isActive = true; + return true + } + + @objc public static func inactivateSession() -> Bool { + if !_isActive { + return true; + } + + do { + try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) + print("Player: Audio Session is Inactive - notifyOthersOnDeactivation") + } catch let error as NSError { + print("Player: Failed to activate Audio Session \(error.localizedDescription)") + return false + } + _isActive = false; + return true + } } diff --git a/packages/player/ios/Classes/Consts.swift b/packages/player/ios/Classes/Consts.swift index fa42c666..84bf2f15 100644 --- a/packages/player/ios/Classes/Consts.swift +++ b/packages/player/ios/Classes/Consts.swift @@ -40,9 +40,9 @@ let ADS_CODE_ERROR_ARGS = "ads_code_error_args" let ADS_MESSAGE_ERROR_ARGS = "ads_message_error_args" let POSITION_ARGS = "position" let SHUFFLE_ARGS = "shuffle_args" -let CURRENT_MEDIA_INDEX_ARGS = "current_media_index_args" let DURATION_ARGS = "duration" let STATE_ARGS = "state" +let CURRENT_MEDIA_INDEX_ARGS = "CURRENT_MEDIA_INDEX" let ITS_AD_TIME = "its_ad_time" let ERROR_ARGS = "error_args" let QUEUE_ARGS = "queue_args" @@ -60,3 +60,4 @@ let NOTIFY_PLAYER_EXTERNAL = "notify_player_external" let STATE_CHANGE = "state_change" let CURRENT_QUEUE = "current_queue" let FAVORITE_UPDATE = "favorite_update" +let SET_CURRENT_MEDIA_INDEX = "SET_CURRENT_MEDIA_INDEX" diff --git a/packages/player/ios/Classes/MethodChannelManager.swift b/packages/player/ios/Classes/MethodChannelManager.swift index 24310e8b..ff78616f 100644 --- a/packages/player/ios/Classes/MethodChannelManager.swift +++ b/packages/player/ios/Classes/MethodChannelManager.swift @@ -42,7 +42,15 @@ public class MethodChannelManager:NSObject{ .build() channel?.invokeMethod("state.change", arguments: args) } -// + + func currentMediaIndex(index: Int) { + let args = MethodChannelManagerArgsBuilder() + .playerId(id:"TAG_ONEPLAYER") + .currentIndex(index:index) + .build() + channel?.invokeMethod(SET_CURRENT_MEDIA_INDEX, arguments: args) + } +// // func repeatModeChanged(repeatMode: Int) { // let args = MethodChannelManagerArgsBuilder() // .event(event: REPEAT_MODE_CHANGED) diff --git a/packages/player/ios/Classes/MethodChannelManagerArgs.swift b/packages/player/ios/Classes/MethodChannelManagerArgs.swift index 5cb88fcd..3f47d8fb 100644 --- a/packages/player/ios/Classes/MethodChannelManagerArgs.swift +++ b/packages/player/ios/Classes/MethodChannelManagerArgs.swift @@ -44,6 +44,11 @@ class MethodChannelManagerArgsBuilder{ return self } + open func currentIndex(index: Int) -> MethodChannelManagerArgsBuilder { + args[CURRENT_MEDIA_INDEX_ARGS] = index + return self + } + // open func queue(queue: Array) -> MethodChannelManagerArgsBuilder { // let queueJson = JsonUtil.toJson(queue) as? String // //TODO: Melhorar o tratamento do null diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index fe72da55..fe5153cb 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -16,7 +16,7 @@ private let tag = TAG private var smPlayer: SMPlayer? = nil - +var currentIndex:Int = 0 public class PlayerPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { @@ -57,19 +57,19 @@ public class PlayerPlugin: NSObject, FlutterPlugin { } } - func convertToMedia(mediaArray: [[String: Any]]) -> [Media]? { - var mediaList: [Media] = [] + func convertToMedia(mediaArray: [[String: Any]]) -> [PlaylistItem]? { + var mediaList: [PlaylistItem] = [] for mediaDict in mediaArray { - + var id = mediaDict["id"] // Certificar que 'id' é um número - let name = mediaDict["name"] + let title = mediaDict["name"] let albumId = mediaDict["albumId"] - let albumTitle = mediaDict["albumTitle"] - let author = mediaDict["author"] + let albumName = mediaDict["albumTitle"] + let artist = mediaDict["author"] let url = mediaDict["url"] let isLocal = mediaDict["isLocal"] - let coverUrl = mediaDict["coverUrl"] + let coverUrl = mediaDict["cover_url"] let bigCoverUrl = mediaDict["bigCoverUrl"] let isVerifiedString = mediaDict["isVerified"] let isVerified = isVerifiedString @@ -80,27 +80,27 @@ public class PlayerPlugin: NSObject, FlutterPlugin { let localPath = mediaDict["localPath"] let playlistId = mediaDict["playlistId"] let fallbackUrl = mediaDict["fallbackUrl"] - + // Criar o objeto Media e adicionar à lista - let media = Media( - id: id as! Int, - name: name as! String, - albumId: albumId as! Int, - albumTitle: albumTitle as! String, - author: author as! String, + let media = PlaylistItem( + albumId:String(albumId as! Int), + albumName: albumName as! String, + title:title as! String, + artist: artist as! String, url: url as! String, - isLocal: isLocal as? Bool ?? false, - localPath: localPath as? String ?? "", coverUrl: coverUrl as? String ?? "", - bigCoverUrl: bigCoverUrl as? String ?? "", - isVerified: isVerified as? Bool ?? false, - playlistId: playlistId as? Int ?? 0, - fallbackUrl: fallbackUrl as? String ?? "" + fallbackUrl: fallbackUrl as? String ?? "", + mediaId: id as! Int, + bigCoverUrl: bigCoverUrl as? String ?? "" ) mediaList.append(media) } return mediaList } + + deinit { + smPlayer?.clearNowPlayingInfo() + } } diff --git a/packages/player/ios/Classes/PlaylistItem.swift b/packages/player/ios/Classes/PlaylistItem.swift index 80d970c0..269fd577 100644 --- a/packages/player/ios/Classes/PlaylistItem.swift +++ b/packages/player/ios/Classes/PlaylistItem.swift @@ -17,7 +17,14 @@ import AVFoundation @objc public var duration: CMTime - @objc public init(albumId: String, albumName: String, title: String, artist: String, url: String, coverUrl: String) { + @objc public var fallbackUrl: String? + + @objc public var mediaId: Int + + @objc public var bigCoverUrl: String? +// @objc public var index: Int + + @objc public init(albumId: String, albumName: String, title: String, artist: String, url: String, coverUrl: String,fallbackUrl: String, mediaId:Int, bigCoverUrl: String) { self.albumId = albumId self.albumName = albumName self.title = title @@ -26,5 +33,9 @@ import AVFoundation self.coverUrl = coverUrl self.duration = .zero self.error = nil + self.fallbackUrl = fallbackUrl + self.mediaId = mediaId + self.bigCoverUrl = bigCoverUrl +// self.index = index } } diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index f40d41a5..00dd565b 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -2,11 +2,13 @@ import Foundation import AVFoundation import MediaPlayer +private var playlistItemKey: UInt8 = 0 + public class SMPlayer : NSObject { var methodChannelManager: MethodChannelManager? private var smPlayer: AVQueuePlayer private var playerItem: AVPlayerItem? - private var queue : [AVPlayerItem] = [] +// private var queue : [PlaylistItem] = [] init(methodChannelManager: MethodChannelManager?) { @@ -15,25 +17,16 @@ public class SMPlayer : NSObject { self.methodChannelManager = methodChannelManager let listeners = SMPlayerListeners(playerItem: playerItem,smPlayer:smPlayer,methodChannelManager:methodChannelManager) listeners.addObservers() - } - - private func setupNowPlaying() { - let commandCenter = MPRemoteCommandCenter.shared() - commandCenter.playCommand.addTarget { [weak self] event in - self?.smPlayer.play() - return .success - } - commandCenter.pauseCommand.addTarget { [weak self] event in - self?.pause() - return .success - } + setupNowPlayingInfoCenter() + + _ = AudioSessionManager.activeSession() } func play(url: String) { guard let videoURL = URL(string: url) else { return } playerItem = AVPlayerItem(url: videoURL) smPlayer.replaceCurrentItem(with: playerItem) - updateNowPlayingInfo() +// updateNowPlayingInfo() smPlayer.play() } @@ -44,20 +37,40 @@ public class SMPlayer : NSObject { func stop() { smPlayer.pause() smPlayer.replaceCurrentItem(with: nil) + clearNowPlayingInfo() } - func enqueue(medias: [Media], autoPlay: Bool, cookie: String) { + func clearNowPlayingInfo() { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + } + + func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { for media in medias { - guard let url = URL(string: media.url) else { continue } - queue.append(AVPlayerItem(url: url)) + guard let url = URL(string: media.url!) else { continue } +// queue.append(media) let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": [ "Cookie": cookie]] let playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) - smPlayer.insert(playerItem, after: nil) + playerItem.playlistItem = media + + smPlayer.insert(playerItem, after: smPlayer.items().isEmpty ? nil : smPlayer.items().last) + + + print("enqueued: \(smPlayer.items().count)") } if(autoPlay){ smPlayer.play() } + NowPlayingCenter.set(item: getCurrentPlaylistItem()) +// methodChannelManager?.currentMediaIndex(index: getCurrentIndex() ?? 0) + } + + func getCurrentIndex() -> Int? { + guard let currentItem = smPlayer.currentItem else { + return nil + } + + return smPlayer.items().firstIndex(of: currentItem) } func next(){ @@ -66,7 +79,7 @@ public class SMPlayer : NSObject { func removeAll(){ smPlayer.removeAllItems() - queue.removeAll() +// queue.removeAll() smPlayer.pause() smPlayer.seek(to: .zero) } @@ -75,17 +88,64 @@ public class SMPlayer : NSObject { smPlayer.play() } - private func updateNowPlayingInfo() { -// guard let playerItem = playerItem else { return } -// let artwork = MPMediaItemArtwork(image: UIImage(named: "queue.first.co)")) -// let nowPlayingInfo: [String: Any] = [ -// MPMediaItemPropertyTitle: "Title", -// MPMediaItemPropertyArtist: "Artist", -// MPMediaItemPropertyArtwork: artwork, -// MPMediaItemPropertyPlaybackDuration: playerItem.asset.duration.seconds, -// MPNowPlayingInfoPropertyElapsedPlaybackTime: smPlayer.currentTime().seconds, -// MPNowPlayingInfoPropertyPlaybackRate: smPlayer.rate -// ] -// MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + func seekToPosition(position:Int){ + + let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: 60000) + smPlayer.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero) + + } + + func getCurrentPlaylistItem() -> PlaylistItem? { + guard let currentItem = smPlayer.currentItem else { + return nil + } + return currentItem.playlistItem + } + + func setupNowPlayingInfoCenter(){ + UIApplication.shared.beginReceivingRemoteControlEvents() + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.nextTrackCommand.isEnabled = true; + commandCenter.previousTrackCommand.isEnabled = true; + commandCenter.changePlaybackPositionCommand.isEnabled = true + + commandCenter.pauseCommand.addTarget { [self]event in + smPlayer.pause() + return .success + } + + commandCenter.playCommand.addTarget { [self]event in + smPlayer.play() + return .success + } + + commandCenter.nextTrackCommand.addTarget {[self]event in + smPlayer.advanceToNextItem() + return .success + } + commandCenter.previousTrackCommand.addTarget {[self]event in + smPlayer.advanceToNextItem() + return .success + } + + commandCenter.changePlaybackPositionCommand.addTarget{[self]event in + let e = event as? MPChangePlaybackPositionCommandEvent + seekToPosition(position: Int((e?.positionTime ?? 0) * 1000)) + return .success + } + } + + + + +} +extension AVPlayerItem { + var playlistItem: PlaylistItem? { + get { + return objc_getAssociatedObject(self, &playlistItemKey) as? PlaylistItem + } + set { + objc_setAssociatedObject(self, &playlistItemKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } } } diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index e1eaa711..723bebdb 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -17,6 +17,9 @@ public class SMPlayerListeners : NSObject { self.playerItem = playerItem self.smPlayer = smPlayer self.methodChannelManager = methodChannelManager + super.init() + self.smPlayer.addObserver(self, forKeyPath: "currentItem", options: [.new, .old], context: nil) + } private var mediaChange: NSKeyValueObservation? @@ -36,19 +39,21 @@ public class SMPlayerListeners : NSObject { let duration : Float64 = CMTimeGetSeconds(smPlayer.currentItem!.duration); if(position < duration){ methodChannelManager?.notifyPositionChange(position: position, duration: duration) + NowPlayingCenter.update(item: smPlayer.currentItem?.playlistItem, rate: 1.0, position: position, duration: duration) } } } - mediaChange = smPlayer.observe(\.currentItem, options: [.new,.old]) { [self] - (player, item) in - if(item.oldValue! != nil && (item.newValue != item.oldValue)){ -// OnePlayerSingleton.i.currentInterval += 1 - } - if(item.newValue != item.oldValue){ - print("onMediaChanged") - } - } + mediaChange = smPlayer.observe(\.currentItem, options: [.new, .old]) { [weak self] (player, change) in + guard let self = self else { return } + + if let newItem = change.newValue, newItem != change.oldValue { + let index = smPlayer.items().firstIndex(of: newItem!) ?? 0 + print("onMediaChanged index: \(index)") + self.methodChannelManager?.currentMediaIndex(index: index) + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) + } + } statusChange = smPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in @@ -86,26 +91,20 @@ public class SMPlayerListeners : NSObject { } }) - playback = smPlayer.observe(\.timeControlStatus, options: [.new, .old], changeHandler: { [self] - (playerItem, change) in - switch (playerItem.timeControlStatus) { - case AVPlayer.TimeControlStatus.paused: - print("observer - paused") -// onePlayerManager?.onStateChange(state: OnePlayerState.PAUSED) - methodChannelManager?.notifyPlayerStateChange(state: PlayerState.paused) - break - case AVPlayer.TimeControlStatus.playing: - print("observer - playing") - methodChannelManager?.notifyPlayerStateChange(state: PlayerState.playing) - break - case AVPlayer.TimeControlStatus.waitingToPlayAtSpecifiedRate: - print("observer - waitingToPlayAtSpecifiedRate") - break - default: - print("observer - default: \(AVPlayer.TimeControlStatus.self)") - break - } - }) + playback = smPlayer.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] (player, change) in + guard let self = self else { return } + + switch player.timeControlStatus { + case .playing: + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.playing) + case .paused: + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.paused) + case .waitingToPlayAtSpecifiedRate: + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + @unknown default: + break + } + } smPlayer.addObserver(self, forKeyPath: "error", options: [.old, .new], context: nil) } @@ -115,6 +114,16 @@ public class SMPlayerListeners : NSObject { print("Erro HTTP: contentIsNotAuthorized") } } + if keyPath == "currentItem" { + if let currentItem = smPlayer.currentItem, let index = smPlayer.items().firstIndex(of: currentItem) { + print("Novo item reproduzido. Índice atual: \(index) | \(smPlayer.currentItem)") + if let currentItem = smPlayer.currentItem, let index = smPlayer.items().firstIndex(of: currentItem) { + print("Item terminou de tocar. Índice atual: \(index) | \(smPlayer.items().count)") + } + } else { + print("Nenhum item atual ou item não encontrado na fila.") + } + } } } From 5f41c7ecf8f9d079847c81078f46e47a077fa809 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 30 Sep 2024 15:32:47 -0300 Subject: [PATCH 47/70] new queue --- packages/player/ios/Classes/SMPlayer.swift | 78 ++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 00dd565b..c78dc64c 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -8,7 +8,10 @@ public class SMPlayer : NSObject { var methodChannelManager: MethodChannelManager? private var smPlayer: AVQueuePlayer private var playerItem: AVPlayerItem? -// private var queue : [PlaylistItem] = [] + public var currentIndex: Int = 0 + private var history: [AVPlayerItem] = [] +// private var currentQueue: [AVPlayerItem] = [] + private var upcomingItems: [AVPlayerItem] = [] init(methodChannelManager: MethodChannelManager?) { @@ -52,8 +55,8 @@ public class SMPlayer : NSObject { let playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) playerItem.playlistItem = media - smPlayer.insert(playerItem, after: smPlayer.items().isEmpty ? nil : smPlayer.items().last) - +// smPlayer.insert(playerItem, after: smPlayer.items().isEmpty ? nil : smPlayer.items().last) + addItem(playerItem) print("enqueued: \(smPlayer.items().count)") } @@ -77,6 +80,51 @@ public class SMPlayer : NSObject { smPlayer.advanceToNextItem() } + func nextTrack() { + if let currentItem = smPlayer.currentItem { + history.append(currentItem) + } + smPlayer.advanceToNextItem() + updateQueue(from:"nextTrack") + NowPlayingCenter.set(item: getCurrentPlaylistItem()) + } + + func previousTrack() { + guard let lastHistoryItem = history.popLast() else { return } + let previousItem = recreatePlayerItem(from: lastHistoryItem) + if let currentItem = smPlayer.currentItem { + let newItem = recreatePlayerItem(from: currentItem) + upcomingItems.insert(newItem, at: 0) + } + + +// upcomingItems.insert(previousItem, at: 0) + smPlayer.replaceCurrentItem(with: previousItem) + updateQueue(from:"previousTrack") + NowPlayingCenter.set(item: getCurrentPlaylistItem()) + } + + private func updateQueue(from:String) { + print("------------------------------------------") + while !upcomingItems.isEmpty { + print("#printStatus upcomingItems: \(String(describing: upcomingItems[0].playlistItem?.title))") + let item = upcomingItems.removeFirst() + let newItem = recreatePlayerItem(from: item) + smPlayer.insert(newItem, after: from == "nextTrack" ? smPlayer.items().last : smPlayer.currentItem) + } + printStatus() + } + + private func recreatePlayerItem(from item: AVPlayerItem) -> AVPlayerItem { + guard let asset = item.asset as? AVURLAsset else { + fatalError("Unable to get asset from AVPlayerItem") + } + let newItem = AVPlayerItem(asset: asset) + newItem.playlistItem = item.playlistItem + return newItem + } + + func removeAll(){ smPlayer.removeAllItems() // queue.removeAll() @@ -88,6 +136,11 @@ public class SMPlayer : NSObject { smPlayer.play() } + func addItem(_ item: AVPlayerItem) { + upcomingItems.append(item) + updateQueue(from:"nextTrack") + } + func seekToPosition(position:Int){ let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: 60000) @@ -135,7 +188,24 @@ public class SMPlayer : NSObject { } } - + func printStatus() { + print("#printStatus #################################################") + print("#printStatus History: \(history.count) items") + for item in history { + print("#printStatus History: \(String(describing: item.playlistItem?.title))") + } + print("#printStatus ------------------------------------------") + print("#printStatus Upcoming Items: \(upcomingItems.count) items") + for item in upcomingItems { + print("#printStatus Upcoming: \(String(describing: item.playlistItem?.title))") + } + print("#printStatus ------------------------------------------") + print("#printStatus AVQueuePlayer items: \(smPlayer.items().count)") + for item in smPlayer.items() { + print("#printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") + } + print("#printStatus #################################################") + } } From a400f50b38be8c35f6474111b423942d56e91154 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 2 Oct 2024 12:41:53 -0300 Subject: [PATCH 48/70] enqueue, playfromqueue, next and previous ok --- packages/player/ios/Classes/Consts.swift | 4 +- packages/player/ios/Classes/Media.swift | 32 -- .../player/ios/Classes/MessageBuffer.swift | 71 ++++ .../ios/Classes/MethodChannelManager.swift | 17 +- .../player/ios/Classes/PlayerPlugin.swift | 69 ++-- .../player/ios/Classes/RepeatManager.swift | 84 +++++ packages/player/ios/Classes/SMPlayer.swift | 348 +++++++++++------- .../ios/Classes/SMPlayerListeners.swift | 161 ++++---- 8 files changed, 500 insertions(+), 286 deletions(-) delete mode 100644 packages/player/ios/Classes/Media.swift create mode 100644 packages/player/ios/Classes/MessageBuffer.swift create mode 100644 packages/player/ios/Classes/RepeatManager.swift diff --git a/packages/player/ios/Classes/Consts.swift b/packages/player/ios/Classes/Consts.swift index 84bf2f15..5a09db37 100644 --- a/packages/player/ios/Classes/Consts.swift +++ b/packages/player/ios/Classes/Consts.swift @@ -48,12 +48,12 @@ let ERROR_ARGS = "error_args" let QUEUE_ARGS = "queue_args" let EXTRAS_ARGS = "extras_args" let IS_FAVORITE_ARGS = "is_favorite_args" -let REPEAT_MODE_ARGS = "repeat_mode_args" +let REPEAT_MODE_ARGS = "REPEAT_MODE" let EVENT_ARGS = "event_args" //EVENTS CHANNEL let POSITION_CHANGE = "position_change" -let REPEAT_MODE_CHANGED = "repeat_mode_changed" +let REPEAT_MODE_CHANGED = "REPEAT_CHANGED" let SHUFFLE_CHANGED = "shuffle_changed" let ON_ADS = "on_ads" let NOTIFY_PLAYER_EXTERNAL = "notify_player_external" diff --git a/packages/player/ios/Classes/Media.swift b/packages/player/ios/Classes/Media.swift deleted file mode 100644 index 622b643b..00000000 --- a/packages/player/ios/Classes/Media.swift +++ /dev/null @@ -1,32 +0,0 @@ -class Media { - let id: Int - let name: String - let albumId: Int - let albumTitle: String - let author: String - var url: String - var isLocal: Bool - let localPath: String? - let coverUrl: String - let bigCoverUrl: String - let isVerified: Bool - let playlistId: Int? - var fallbackUrl: String? - - // Inicializador para a classe - init(id: Int, name: String, albumId: Int, albumTitle: String, author: String, url: String, isLocal: Bool, localPath: String?, coverUrl: String, bigCoverUrl: String, isVerified: Bool, playlistId: Int?, fallbackUrl: String?) { - self.id = id - self.name = name - self.albumId = albumId - self.albumTitle = albumTitle - self.author = author - self.url = url - self.isLocal = isLocal - self.localPath = localPath - self.coverUrl = coverUrl - self.bigCoverUrl = bigCoverUrl - self.isVerified = isVerified - self.playlistId = playlistId - self.fallbackUrl = fallbackUrl - } -} diff --git a/packages/player/ios/Classes/MessageBuffer.swift b/packages/player/ios/Classes/MessageBuffer.swift new file mode 100644 index 00000000..4a23b230 --- /dev/null +++ b/packages/player/ios/Classes/MessageBuffer.swift @@ -0,0 +1,71 @@ +// +// MessageBuffer.swift +// smplayer +// +// Created by Lucas Tonussi on 01/10/24. +// +import Foundation +import AVFoundation + +class MessageBuffer { + static let shared = MessageBuffer() + + private let queue = DispatchQueue(label: "suamusica.messagebuffer", attributes: .concurrent) + private var buffer: [PlaylistItem] = [] + private var bufferUnique: AVPlayerItem? = nil + private let bufferSize = 10 + + private init() {} + + func sendUnique(_ message: AVPlayerItem) { + queue.async(flags: .barrier) { + if self.buffer.count >= self.bufferSize { + self.buffer.removeFirst() + print("MessageBuffer: Removido o primeiro item do buffer.") + } + self.bufferUnique = message + print("MessageBuffer: Adicionado item unico Buffer atual: \(message.playlistItem?.title)") + } + } + + func receiveUnique() -> AVPlayerItem? { + var result: AVPlayerItem? + queue.sync { + if (self.bufferUnique != nil) { + result = self.bufferUnique + print("MessageBuffer: Recebido \(String(describing: result?.playlistItem?.title))") + self.bufferUnique = nil + print("MessageBuffer: BufferUnique limpo após recebimento.") + } else { + print("MessageBuffer: BufferUnique vazio.") + } + } + return result + } + + func send(_ message: [PlaylistItem]) { + queue.async(flags: .barrier) { + if self.buffer.count >= self.bufferSize { + self.buffer.removeFirst() + print("MessageBuffer: Removido o primeiro item do buffer.") + } + self.buffer.append(contentsOf: message) + print("MessageBuffer: Adicionado \(message.count) itens. Buffer atual: \(self.buffer.count)") + } + } + + func receive() -> [PlaylistItem]? { + var result: [PlaylistItem]? + queue.sync { + if !self.buffer.isEmpty { + result = self.buffer + print("MessageBuffer: Recebido \(result?.count ?? 0) itens.") + self.buffer.removeAll() + print("MessageBuffer: Buffer limpo após recebimento.") + } else { + print("MessageBuffer: Buffer vazio.") + } + } + return result + } +} diff --git a/packages/player/ios/Classes/MethodChannelManager.swift b/packages/player/ios/Classes/MethodChannelManager.swift index ff78616f..530e63e0 100644 --- a/packages/player/ios/Classes/MethodChannelManager.swift +++ b/packages/player/ios/Classes/MethodChannelManager.swift @@ -30,7 +30,7 @@ public class MethodChannelManager:NSObject{ .duration(duration:Int(duration)*1000) // .currentMediaIndex(currentMediaIndex:currentMediaIndex) .build() - print("notifyPositionChange no tempo: \(position) segundos | duration: \(duration)") +// print("notifyPositionChange no tempo: \(position) segundos | duration: \(duration)") channel?.invokeMethod("audio.onCurrentPosition", arguments: args) } @@ -50,14 +50,13 @@ public class MethodChannelManager:NSObject{ .build() channel?.invokeMethod(SET_CURRENT_MEDIA_INDEX, arguments: args) } -// -// func repeatModeChanged(repeatMode: Int) { -// let args = MethodChannelManagerArgsBuilder() -// .event(event: REPEAT_MODE_CHANGED) -// .repeatMode(repeatMode: repeatMode) -// .build() -// eventSink?(args) -// } + + func repeatModeChanged(repeatMode: Int) { + let args = MethodChannelManagerArgsBuilder() + .repeatMode(repeatMode: repeatMode) + .build() + channel?.invokeMethod(REPEAT_MODE_CHANGED, arguments: args) + } // // func shuffleChanged(shuffleIsActive: Bool) { // let args = MethodChannelManagerArgsBuilder() diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index fe5153cb..b73781f7 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -15,7 +15,6 @@ private let tag = TAG private var smPlayer: SMPlayer? = nil - var currentIndex:Int = 0 public class PlayerPlugin: NSObject, FlutterPlugin { @@ -29,20 +28,24 @@ public class PlayerPlugin: NSObject, FlutterPlugin { } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - print("call.method: \(call.method)") + print("call.method: \(call.method)") switch call.method { case "enqueue": if let batch = call.arguments as? [String: Any], - let listMedia = batch["batch"] as? [[String: Any]]{ + let listMedia = batch["batch"] as? [[String: Any]] { let autoPlay = batch["autoPlay"] as? Bool ?? false let cookie = batch["cookie"] as? String ?? "" - if convertToMedia(mediaArray: listMedia) != nil { - smPlayer?.enqueue(medias: convertToMedia(mediaArray: listMedia)!, autoPlay: autoPlay,cookie: cookie) - } + if let mediaList = convertToMedia(mediaArray: listMedia) { + MessageBuffer.shared.send(mediaList) + smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie) + } } result(NSNumber(value: true)) case "next": - smPlayer?.next() + smPlayer?.nextTrack() + result(NSNumber(value: 1)) + case "previous": + smPlayer?.previousTrack() result(NSNumber(value: 1)) case "play": smPlayer?.play() @@ -50,35 +53,43 @@ public class PlayerPlugin: NSObject, FlutterPlugin { case "pause": smPlayer?.pause() result(NSNumber(value: 1)) + case "disable_repeat_mode": + smPlayer?.disableRepeatMode() + result(NSNumber(value: 1)) + case "playFromQueue": + if let args = call.arguments as? [String: Any] { + smPlayer?.playFromQueue(position: args["position"] as? Int ?? 0, timePosition: args["timePosition"] as? Int ?? 0, loadOnly: args["loadOnly"] as? Bool ?? false) + } + result(NSNumber(value: 1)) case "remove_all": result(NSNumber(value: true)) + case "seek": + if let args = call.arguments as? [String: Any] { + let position = args["position"] ?? 0 + smPlayer?.seek(position:position as! Int) + } + result(NSNumber(value: 1)) default: - result(NSNumber(value: false)) + result(NSNumber(value: 0)) } } func convertToMedia(mediaArray: [[String: Any]]) -> [PlaylistItem]? { + let enqueueStartTime = Date() var mediaList: [PlaylistItem] = [] for mediaDict in mediaArray { - - var id = mediaDict["id"] // Certificar que 'id' é um número - let title = mediaDict["name"] - let albumId = mediaDict["albumId"] - let albumName = mediaDict["albumTitle"] - let artist = mediaDict["author"] - let url = mediaDict["url"] - let isLocal = mediaDict["isLocal"] - let coverUrl = mediaDict["cover_url"] - let bigCoverUrl = mediaDict["bigCoverUrl"] - let isVerifiedString = mediaDict["isVerified"] - let isVerified = isVerifiedString - - - - // Atribuição opcional - let localPath = mediaDict["localPath"] - let playlistId = mediaDict["playlistId"] + + let id = mediaDict["id"] + let title = mediaDict["name"] + let albumId = mediaDict["albumId"] + let albumName = mediaDict["albumTitle"] + let artist = mediaDict["author"] + let url = mediaDict["url"] + let isLocal = mediaDict["isLocal"] + let coverUrl = mediaDict["cover_url"] + let bigCoverUrl = mediaDict["bigCoverUrl"] + let isVerifiedString = mediaDict["isVerified"] let fallbackUrl = mediaDict["fallbackUrl"] // Criar o objeto Media e adicionar à lista @@ -95,12 +106,14 @@ public class PlayerPlugin: NSObject, FlutterPlugin { ) mediaList.append(media) } - + let enqueueEndTime = Date() + let enqueueTime = enqueueEndTime.timeIntervalSince(enqueueStartTime) + print("PlayerPlugin: convertToMedia concluído em \(enqueueTime) segundos mediaArray: \(mediaArray.count)") return mediaList } deinit { smPlayer?.clearNowPlayingInfo() } - + } diff --git a/packages/player/ios/Classes/RepeatManager.swift b/packages/player/ios/Classes/RepeatManager.swift new file mode 100644 index 00000000..a7ab70a6 --- /dev/null +++ b/packages/player/ios/Classes/RepeatManager.swift @@ -0,0 +1,84 @@ +import AVFoundation + +// Gerenciador de repetição +public final class RepeatManager: NSObject { + weak var player: AVQueuePlayer? + + var mode: AVQueuePlayer.RepeatMode = .REPEAT_MODE_OFF + + init(player: AVQueuePlayer) { + self.player = player + super.init() + + startObservingCurrentItem(of: player) + if let playerItem = player.currentItem { + startObservingNotifications(of: playerItem) + } + } + + deinit { + guard let player = player else { + return + } + + stopObservingCurrentItem(of: player) + if let playerItem = player.currentItem { + stopObservingNotifications(of: playerItem) + } + } + + func startObservingCurrentItem(of player: AVPlayer) { + player.addObserver(self, forKeyPath: "currentItem", options: [.old, .new], context: nil) + } + + func stopObservingCurrentItem(of player: AVPlayer) { + player.removeObserver(self, forKeyPath: "currentItem") + } + + func startObservingNotifications(of playerItem: AVPlayerItem) { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleItemDidPlayToEnd(notification:)), + name: .AVPlayerItemDidPlayToEndTime, + object: playerItem + ) + } + + func stopObservingNotifications(of playerItem: AVPlayerItem) { + NotificationCenter.default.removeObserver( + self, + name: .AVPlayerItemDidPlayToEndTime, + object: playerItem + ) + } + + @objc func handleItemDidPlayToEnd(notification: Notification) { + guard let player = player, let currentItem = player.currentItem, currentItem == notification.object as? AVPlayerItem else { + return + } + + switch mode { + case .REPEAT_MODE_ALL: + player.advanceToNextItem() + player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) + player.insert(currentItem, after: nil) + case .REPEAT_MODE_ONE: + player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) + default: + break + } + } + + public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "currentItem", let player = object as? AVPlayer { + if let oldItem = change?[.oldKey] as? AVPlayerItem { + stopObservingNotifications(of: oldItem) + } + if let newItem = change?[.newKey] as? AVPlayerItem { + startObservingNotifications(of: newItem) + } + } + } +} + + diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index c78dc64c..687f22cb 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -8,35 +8,65 @@ public class SMPlayer : NSObject { var methodChannelManager: MethodChannelManager? private var smPlayer: AVQueuePlayer private var playerItem: AVPlayerItem? - public var currentIndex: Int = 0 - private var history: [AVPlayerItem] = [] -// private var currentQueue: [AVPlayerItem] = [] - private var upcomingItems: [AVPlayerItem] = [] + private var historyQueue: [AVPlayerItem] = [] + private var futureQueue: [AVPlayerItem] = [] + var currentQueue: [AVPlayerItem] { + return historyQueue + smPlayer.items() + futureQueue + } init(methodChannelManager: MethodChannelManager?) { smPlayer = AVQueuePlayer() + super.init() self.methodChannelManager = methodChannelManager let listeners = SMPlayerListeners(playerItem: playerItem,smPlayer:smPlayer,methodChannelManager:methodChannelManager) - listeners.addObservers() + listeners.addObservers() + listeners.onMediaChanged = { + methodChannelManager?.currentMediaIndex(index: self.historyQueue.count) + } setupNowPlayingInfoCenter() - _ = AudioSessionManager.activeSession() - } + _ = RepeatManager(player:smPlayer) + NotificationCenter.default.addObserver(self, selector: #selector(itemDidFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: smPlayer.currentItem) + } + func play(url: String) { guard let videoURL = URL(string: url) else { return } playerItem = AVPlayerItem(url: videoURL) smPlayer.replaceCurrentItem(with: playerItem) -// updateNowPlayingInfo() + // updateNowPlayingInfo() smPlayer.play() } - + func pause() { smPlayer.pause() } - + + + func disableRepeatMode() { + smPlayer.repeatMode = .REPEAT_MODE_OFF + } + + func seek(position:Int){ + let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: 60000) + smPlayer.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero) + } + + func toggleRepeatMode() { + switch smPlayer.repeatMode { + case .REPEAT_MODE_OFF: + smPlayer.repeatMode = .REPEAT_MODE_ALL + case .REPEAT_MODE_ALL: + smPlayer.repeatMode = .REPEAT_MODE_ONE + case .REPEAT_MODE_ONE: + smPlayer.repeatMode = .REPEAT_MODE_OFF + } + methodChannelManager?.repeatModeChanged(repeatMode: smPlayer.repeatMode.hashValue) + } + + func stop() { smPlayer.pause() smPlayer.replaceCurrentItem(with: nil) @@ -48,86 +78,76 @@ public class SMPlayer : NSObject { } func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { - for media in medias { - guard let url = URL(string: media.url!) else { continue } -// queue.append(media) - let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": [ "Cookie": cookie]] - let playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) - playerItem.playlistItem = media - -// smPlayer.insert(playerItem, after: smPlayer.items().isEmpty ? nil : smPlayer.items().last) - addItem(playerItem) - - print("enqueued: \(smPlayer.items().count)") - } - - if(autoPlay){ - smPlayer.play() - } - NowPlayingCenter.set(item: getCurrentPlaylistItem()) -// methodChannelManager?.currentMediaIndex(index: getCurrentIndex() ?? 0) + guard let message = MessageBuffer.shared.receive() else { return } + let isFirstBatch = self.smPlayer.items().count == 0 + for media in message { + guard let url = URL(string: media.url!) else { continue } + let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie]] + let playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) + playerItem.playlistItem = media + self.addItem(playerItem) + } + if autoPlay && isFirstBatch { + self.smPlayer.play() + self.setNowPlaying() + } } func getCurrentIndex() -> Int? { guard let currentItem = smPlayer.currentItem else { return nil } - + return smPlayer.items().firstIndex(of: currentItem) } - func next(){ + func nextTrack() { + if let currentItem = smPlayer.currentItem { + historyQueue.append(currentItem) + } smPlayer.advanceToNextItem() + setNowPlaying() + insertIntoPlayerIfNeeded() } - func nextTrack() { - if let currentItem = smPlayer.currentItem { - history.append(currentItem) - } - smPlayer.advanceToNextItem() - updateQueue(from:"nextTrack") - NowPlayingCenter.set(item: getCurrentPlaylistItem()) - } - func previousTrack() { - guard let lastHistoryItem = history.popLast() else { return } - let previousItem = recreatePlayerItem(from: lastHistoryItem) - if let currentItem = smPlayer.currentItem { - let newItem = recreatePlayerItem(from: currentItem) - upcomingItems.insert(newItem, at: 0) - } - - -// upcomingItems.insert(previousItem, at: 0) - smPlayer.replaceCurrentItem(with: previousItem) - updateQueue(from:"previousTrack") + //TESTE com pause + guard let lastHistoryItem = historyQueue.popLast() else { return /*smPlayer.seek(to: CMTime.zero) */} + let currentItem = smPlayer.currentItem + let lastItemInPlayer = smPlayer.items().last! + smPlayer.remove(lastItemInPlayer) + futureQueue.insert(lastItemInPlayer, at: 0) + smPlayer.insert(lastHistoryItem, after: currentItem!) + smPlayer.advanceToNextItem() + smPlayer.insert(currentItem!, after: smPlayer.currentItem) + printStatus() + seekToPosition(position: 0) + setNowPlaying() + } + + func setNowPlaying(){ NowPlayingCenter.set(item: getCurrentPlaylistItem()) } - - private func updateQueue(from:String) { - print("------------------------------------------") - while !upcomingItems.isEmpty { - print("#printStatus upcomingItems: \(String(describing: upcomingItems[0].playlistItem?.title))") - let item = upcomingItems.removeFirst() - let newItem = recreatePlayerItem(from: item) - smPlayer.insert(newItem, after: from == "nextTrack" ? smPlayer.items().last : smPlayer.currentItem) - } - printStatus() - } - - private func recreatePlayerItem(from item: AVPlayerItem) -> AVPlayerItem { - guard let asset = item.asset as? AVURLAsset else { - fatalError("Unable to get asset from AVPlayerItem") - } - let newItem = AVPlayerItem(asset: asset) - newItem.playlistItem = item.playlistItem - return newItem + + private func insertIntoPlayerIfNeeded() { + let maxTotalItems = 5 + let currentItemCount = smPlayer.items().count + let itemsToAdd = min(maxTotalItems - currentItemCount, futureQueue.count) + + for _ in 0.. PlaylistItem? { - guard let currentItem = smPlayer.currentItem else { - return nil - } + guard let currentItem = smPlayer.currentItem else { + return nil + } return currentItem.playlistItem + } + + func playFromQueue(position: Int, timePosition: Int, loadOnly: Bool) { + let allItems = currentQueue + smPlayer.removeAllItems() + historyQueue.removeAll() + futureQueue.removeAll() + for (index, item) in allItems.enumerated() { + if index < position { + historyQueue.append(item) + } else { + futureQueue.append(item) +// MessageBuffer.shared.sendUnique(item) + } + } + insertIntoPlayerIfNeeded() + if(loadOnly){ + seekToPosition(position: timePosition) + smPlayer.pause() } + } - func setupNowPlayingInfoCenter(){ - UIApplication.shared.beginReceivingRemoteControlEvents() - let commandCenter = MPRemoteCommandCenter.shared() - commandCenter.nextTrackCommand.isEnabled = true; - commandCenter.previousTrackCommand.isEnabled = true; - commandCenter.changePlaybackPositionCommand.isEnabled = true - - commandCenter.pauseCommand.addTarget { [self]event in - smPlayer.pause() - return .success - } - - commandCenter.playCommand.addTarget { [self]event in - smPlayer.play() - return .success - } - - commandCenter.nextTrackCommand.addTarget {[self]event in - smPlayer.advanceToNextItem() - return .success - } - commandCenter.previousTrackCommand.addTarget {[self]event in - smPlayer.advanceToNextItem() - return .success - } - - commandCenter.changePlaybackPositionCommand.addTarget{[self]event in - let e = event as? MPChangePlaybackPositionCommandEvent - seekToPosition(position: Int((e?.positionTime ?? 0) * 1000)) - return .success - } - } + func setupNowPlayingInfoCenter(){ + UIApplication.shared.beginReceivingRemoteControlEvents() + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.nextTrackCommand.isEnabled = true; + commandCenter.previousTrackCommand.isEnabled = true; + commandCenter.changePlaybackPositionCommand.isEnabled = true + + commandCenter.pauseCommand.addTarget { [self]event in + smPlayer.pause() + return .success + } + + commandCenter.playCommand.addTarget { [self]event in + smPlayer.play() + return .success + } + + commandCenter.nextTrackCommand.addTarget {[self]event in + smPlayer.advanceToNextItem() + return .success + } + commandCenter.previousTrackCommand.addTarget {[self]event in + smPlayer.advanceToNextItem() + return .success + } + + commandCenter.changePlaybackPositionCommand.addTarget{[self]event in + let e = event as? MPChangePlaybackPositionCommandEvent + seekToPosition(position: Int((e?.positionTime ?? 0) * 1000)) + return .success + } + } func printStatus() { print("#printStatus #################################################") - print("#printStatus History: \(history.count) items") - for item in history { - print("#printStatus History: \(String(describing: item.playlistItem?.title))") - } - print("#printStatus ------------------------------------------") - print("#printStatus Upcoming Items: \(upcomingItems.count) items") - for item in upcomingItems { - print("#printStatus Upcoming: \(String(describing: item.playlistItem?.title))") - } - print("#printStatus ------------------------------------------") - print("#printStatus AVQueuePlayer items: \(smPlayer.items().count)") - for item in smPlayer.items() { - print("#printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") - } - print("#printStatus #################################################") + print("#printStatus History: \(historyQueue.count) items") + for item in historyQueue { + print("#printStatus History: \(String(describing: item.playlistItem?.title))") } + print("#printStatus ------------------------------------------") + print("#printStatus futureQueue Items: \(futureQueue.count) items") +// for item in futureQueue { +// print("#printStatus Upcoming: \(String(describing: item.playlistItem?.title))") +// } + print("#printStatus ------------------------------------------") + print("#printStatus AVQueuePlayer items: \(smPlayer.items().count)") + for item in smPlayer.items() { + print("#printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") + } + print("#printStatus #################################################") + } - + //override automatic next + @objc func itemDidFinishPlaying(_ notification: Notification) { + smPlayer.pause() + nextTrack() + smPlayer.play() + } } extension AVPlayerItem { var playlistItem: PlaylistItem? { @@ -219,3 +264,48 @@ extension AVPlayerItem { } } } + + +// Extensão de AVPlayer para suporte a modo de repetição +public extension AVQueuePlayer { + private struct CustomProperties { + static var repeatManager: UInt8 = 0 + } + + private var repeatManager: RepeatManager? { + get { + return objc_getAssociatedObject(self, &CustomProperties.repeatManager) as? RepeatManager + } + set(value) { + objc_setAssociatedObject(self, &CustomProperties.repeatManager, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + enum RepeatMode { + case REPEAT_MODE_OFF + case REPEAT_MODE_ONE + case REPEAT_MODE_ALL + } + + var repeatMode: RepeatMode { + get { + return repeatManager?.mode ?? .REPEAT_MODE_OFF + } + set(mode) { + if repeatManager == nil { + repeatManager = RepeatManager(player: self ) + } + repeatManager?.mode = mode + + switch mode { + case .REPEAT_MODE_OFF: + actionAtItemEnd = .none + case .REPEAT_MODE_ALL: + actionAtItemEnd = .advance + default: + actionAtItemEnd = .pause + } + } + } + +} diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index 723bebdb..e6d4eb93 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -13,15 +13,17 @@ public class SMPlayerListeners : NSObject { let smPlayer:AVQueuePlayer let methodChannelManager: MethodChannelManager? + var onMediaChanged: (() -> Void)? + init(playerItem: AVPlayerItem?, smPlayer: AVQueuePlayer,methodChannelManager: MethodChannelManager?) { self.playerItem = playerItem self.smPlayer = smPlayer self.methodChannelManager = methodChannelManager super.init() - self.smPlayer.addObserver(self, forKeyPath: "currentItem", options: [.new, .old], context: nil) - + + } - + private var mediaChange: NSKeyValueObservation? private var statusChange: NSKeyValueObservation? private var loading: NSKeyValueObservation? @@ -32,81 +34,79 @@ public class SMPlayerListeners : NSObject { func addObservers() { let interval = CMTime(seconds: 0.5, - preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [self] time in - let position : Float64 = CMTimeGetSeconds(smPlayer.currentTime()); - if(smPlayer.currentItem != nil){ - let duration : Float64 = CMTimeGetSeconds(smPlayer.currentItem!.duration); - if(position < duration){ - methodChannelManager?.notifyPositionChange(position: position, duration: duration) - NowPlayingCenter.update(item: smPlayer.currentItem?.playlistItem, rate: 1.0, position: position, duration: duration) - } - } + preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [self] time in + let position : Float64 = CMTimeGetSeconds(smPlayer.currentTime()); + if(smPlayer.currentItem != nil){ + let duration : Float64 = CMTimeGetSeconds(smPlayer.currentItem!.duration); + if(position < duration){ + methodChannelManager?.notifyPositionChange(position: position, duration: duration) + NowPlayingCenter.update(item: smPlayer.currentItem?.playlistItem, rate: 1.0, position: position, duration: duration) } - + } + } + mediaChange = smPlayer.observe(\.currentItem, options: [.new, .old]) { [weak self] (player, change) in - guard let self = self else { return } - - if let newItem = change.newValue, newItem != change.oldValue { - let index = smPlayer.items().firstIndex(of: newItem!) ?? 0 - print("onMediaChanged index: \(index)") - self.methodChannelManager?.currentMediaIndex(index: index) - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) - } - } - - statusChange = smPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { - (playerItem, change) in - if playerItem.status == .readyToPlay { - print("readyToPlay") - } else if playerItem.status == .failed { - print("failed") - } - }) - - loading = smPlayer.currentItem?.observe(\.isPlaybackBufferEmpty, options: [.new,.old]) { [self] - (new, old) in - print("observer - loading") - methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) - } - - loaded = smPlayer.currentItem?.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { - (player, _) in -// OnePlayerSingleton.i.log(tag: tag, message: "observer - loaded \(player)") - print("observer - loaded") - } - - - notPlayingReason = smPlayer.observe(\.reasonForWaitingToPlay, options: [.new], changeHandler: { [self] - (playerItem, change) in - switch (smPlayer.reasonForWaitingToPlay) { - case AVPlayer.WaitingReason.evaluatingBufferingRate: - print("evaluatingBufferingRate") - case AVPlayer.WaitingReason.toMinimizeStalls: - print("toMinimizeStalls") - case AVPlayer.WaitingReason.noItemToPlay: - print("noItemToPlay") - default: - print("default") - } - }) - + guard let self = self else { return } + + if let newItem = change.newValue, newItem != change.oldValue { + onMediaChanged?() + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) + } + } + + statusChange = smPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { + (playerItem, change) in + if playerItem.status == .readyToPlay { + print("readyToPlay") + } else if playerItem.status == .failed { + print("failed") + } + }) + + loading = smPlayer.currentItem?.observe(\.isPlaybackBufferEmpty, options: [.new,.old]) { [self] + (new, old) in + print("observer - loading") + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + } + + loaded = smPlayer.currentItem?.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { + (player, _) in + + print("observer - loaded") + } + + + notPlayingReason = smPlayer.observe(\.reasonForWaitingToPlay, options: [.new], changeHandler: { [self] + (playerItem, change) in + switch (smPlayer.reasonForWaitingToPlay) { + case AVPlayer.WaitingReason.evaluatingBufferingRate: + print("evaluatingBufferingRate") + case AVPlayer.WaitingReason.toMinimizeStalls: + print("toMinimizeStalls") + case AVPlayer.WaitingReason.noItemToPlay: + print("noItemToPlay") + default: + print("default") + } + }) + playback = smPlayer.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] (player, change) in - guard let self = self else { return } - - switch player.timeControlStatus { - case .playing: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.playing) - case .paused: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.paused) - case .waitingToPlayAtSpecifiedRate: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) - @unknown default: - break - } - } - - smPlayer.addObserver(self, forKeyPath: "error", options: [.old, .new], context: nil) + guard let self = self else { return } + + switch player.timeControlStatus { + case .playing: + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.playing) + case .paused: + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.paused) + case .waitingToPlayAtSpecifiedRate: + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + @unknown default: + break + } + } + + smPlayer.addObserver(self, forKeyPath: "error", options: [.old, .new], context: nil) } public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "error", let change = change, let error = change[.newKey] as? Error { @@ -114,16 +114,5 @@ public class SMPlayerListeners : NSObject { print("Erro HTTP: contentIsNotAuthorized") } } - if keyPath == "currentItem" { - if let currentItem = smPlayer.currentItem, let index = smPlayer.items().firstIndex(of: currentItem) { - print("Novo item reproduzido. Índice atual: \(index) | \(smPlayer.currentItem)") - if let currentItem = smPlayer.currentItem, let index = smPlayer.items().firstIndex(of: currentItem) { - print("Item terminou de tocar. Índice atual: \(index) | \(smPlayer.items().count)") - } - } else { - print("Nenhum item atual ou item não encontrado na fila.") - } - } } - } From 291a4e8f496492d6ef85bf700e3cae8dd475035f Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 7 Oct 2024 15:49:52 -0300 Subject: [PATCH 49/70] fix reorder and shuffle --- packages/player/ios/Classes/Consts.swift | 4 +- packages/player/ios/Classes/CoverCenter.swift | 3 + .../ios/Classes/MethodChannelManager.swift | 15 +- .../player/ios/Classes/PlayerPlugin.swift | 16 +- packages/player/ios/Classes/SMPlayer.swift | 195 +++++++++++------- 5 files changed, 152 insertions(+), 81 deletions(-) diff --git a/packages/player/ios/Classes/Consts.swift b/packages/player/ios/Classes/Consts.swift index 5a09db37..bf13f349 100644 --- a/packages/player/ios/Classes/Consts.swift +++ b/packages/player/ios/Classes/Consts.swift @@ -39,7 +39,7 @@ let ADS_EVENT_IS_AUDIO = "ads_event_is_audio" let ADS_CODE_ERROR_ARGS = "ads_code_error_args" let ADS_MESSAGE_ERROR_ARGS = "ads_message_error_args" let POSITION_ARGS = "position" -let SHUFFLE_ARGS = "shuffle_args" +let SHUFFLE_ARGS = "SHUFFLE_MODE" let DURATION_ARGS = "duration" let STATE_ARGS = "state" let CURRENT_MEDIA_INDEX_ARGS = "CURRENT_MEDIA_INDEX" @@ -54,7 +54,7 @@ let EVENT_ARGS = "event_args" //EVENTS CHANNEL let POSITION_CHANGE = "position_change" let REPEAT_MODE_CHANGED = "REPEAT_CHANGED" -let SHUFFLE_CHANGED = "shuffle_changed" +let SHUFFLE_CHANGED = "SHUFFLE_CHANGED" let ON_ADS = "on_ads" let NOTIFY_PLAYER_EXTERNAL = "notify_player_external" let STATE_CHANGE = "state_change" diff --git a/packages/player/ios/Classes/CoverCenter.swift b/packages/player/ios/Classes/CoverCenter.swift index 6478b405..85ba727a 100644 --- a/packages/player/ios/Classes/CoverCenter.swift +++ b/packages/player/ios/Classes/CoverCenter.swift @@ -59,6 +59,9 @@ extension String { } else { saveToLocalCache(item: item, url: url, data: data!) } + if(data == nil){ + return nil + } } let image = self.toUIImage(data: Data.init(referencing: data!), url: url) diff --git a/packages/player/ios/Classes/MethodChannelManager.swift b/packages/player/ios/Classes/MethodChannelManager.swift index 530e63e0..b7f1dcfb 100644 --- a/packages/player/ios/Classes/MethodChannelManager.swift +++ b/packages/player/ios/Classes/MethodChannelManager.swift @@ -57,13 +57,12 @@ public class MethodChannelManager:NSObject{ .build() channel?.invokeMethod(REPEAT_MODE_CHANGED, arguments: args) } -// -// func shuffleChanged(shuffleIsActive: Bool) { -// let args = MethodChannelManagerArgsBuilder() -// .event(event: SHUFFLE_CHANGED) -// .shuffleIsActive(shuffleIsActive: shuffleIsActive) -// .build() -// eventSink?(args) -// } + + func shuffleChanged(shuffleIsActive: Bool) { + let args = MethodChannelManagerArgsBuilder() + .shuffleIsActive(shuffleIsActive: shuffleIsActive) + .build() + channel?.invokeMethod(SHUFFLE_CHANGED, arguments: args) + } } diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index b73781f7..5ac523ca 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -15,8 +15,6 @@ private let tag = TAG private var smPlayer: SMPlayer? = nil -var currentIndex:Int = 0 - public class PlayerPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: registrar.messenger()) @@ -53,6 +51,11 @@ public class PlayerPlugin: NSObject, FlutterPlugin { case "pause": smPlayer?.pause() result(NSNumber(value: 1)) + case "toggle_shuffle": + if let args = call.arguments as? [String: Any]{ + smPlayer?.toggleShuffle(positionsList: args["positionsList"] as! [[String: Int]]) + } + result(NSNumber(value: 1)) case "disable_repeat_mode": smPlayer?.disableRepeatMode() result(NSNumber(value: 1)) @@ -62,6 +65,15 @@ public class PlayerPlugin: NSObject, FlutterPlugin { } result(NSNumber(value: 1)) case "remove_all": + smPlayer?.removeAll() + result(NSNumber(value: true)) + case "reorder": + if let args = call.arguments as? [String: Any], + let oldIndex = args["oldIndex"] as? Int, + let newIndex = args["newIndex"] as? Int, + let positionsList = args["positionsList"] as? [[String : Int]] { + smPlayer?.reorder(fromIndex: oldIndex, toIndex: newIndex,positionsList: positionsList) + } result(NSNumber(value: true)) case "seek": if let args = call.arguments as? [String: Any] { diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 687f22cb..c32f88ac 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -6,18 +6,30 @@ private var playlistItemKey: UInt8 = 0 public class SMPlayer : NSObject { var methodChannelManager: MethodChannelManager? + //Queue handle private var smPlayer: AVQueuePlayer private var playerItem: AVPlayerItem? private var historyQueue: [AVPlayerItem] = [] private var futureQueue: [AVPlayerItem] = [] + //Shuffle handle + private var originalQueue: [AVPlayerItem] = [] + private var shuffledIndices: [Int] = [] + private var isShuffleModeEnabled: Bool = false + var shuffledQueue: [AVPlayerItem] = [] - var currentQueue: [AVPlayerItem] { - return historyQueue + smPlayer.items() + futureQueue + var fullQueue: [AVPlayerItem] { + return historyQueue + smPlayer.items() + futureQueue + } + + var currentIndex : Int? { + guard let currentItem = smPlayer.currentItem else { + return nil + } + return fullQueue.firstIndex(of: currentItem) } init(methodChannelManager: MethodChannelManager?) { smPlayer = AVQueuePlayer() - super.init() self.methodChannelManager = methodChannelManager let listeners = SMPlayerListeners(playerItem: playerItem,smPlayer:smPlayer,methodChannelManager:methodChannelManager) @@ -29,15 +41,7 @@ public class SMPlayer : NSObject { _ = AudioSessionManager.activeSession() _ = RepeatManager(player:smPlayer) NotificationCenter.default.addObserver(self, selector: #selector(itemDidFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: smPlayer.currentItem) - - } - - func play(url: String) { - guard let videoURL = URL(string: url) else { return } - playerItem = AVPlayerItem(url: videoURL) - smPlayer.replaceCurrentItem(with: playerItem) - // updateNowPlayingInfo() - smPlayer.play() + } func pause() { @@ -47,6 +51,7 @@ public class SMPlayer : NSObject { func disableRepeatMode() { smPlayer.repeatMode = .REPEAT_MODE_OFF + methodChannelManager?.repeatModeChanged(repeatMode: smPlayer.repeatMode.hashValue) } func seek(position:Int){ @@ -78,27 +83,51 @@ public class SMPlayer : NSObject { } func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { - guard let message = MessageBuffer.shared.receive() else { return } - let isFirstBatch = self.smPlayer.items().count == 0 - for media in message { - guard let url = URL(string: media.url!) else { continue } - let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie]] - let playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) - playerItem.playlistItem = media - self.addItem(playerItem) - } - if autoPlay && isFirstBatch { - self.smPlayer.play() - self.setNowPlaying() - } + guard let message = MessageBuffer.shared.receive() else { return } + let isFirstBatch = self.smPlayer.items().count == 0 + for media in message { + guard let url = URL(string: media.url!) else { continue } + let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie]] + let playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) + playerItem.playlistItem = media + futureQueue.append(playerItem) + } + insertIntoPlayerIfNeeded() + if autoPlay && isFirstBatch { + self.smPlayer.play() + self.setNowPlaying() + } } - func getCurrentIndex() -> Int? { - guard let currentItem = smPlayer.currentItem else { - return nil + func toggleShuffle(positionsList: [[String: Int]]) { + isShuffleModeEnabled.toggle() + if isShuffleModeEnabled { + shuffledIndices = positionsList.compactMap { $0["originalPosition"] } + originalQueue = fullQueue + fillShuffledQueue() + distributeItemsInRightQueue(currentQueue: shuffledQueue) + } else { + if(!originalQueue.isEmpty){ + distributeItemsInRightQueue(currentQueue: originalQueue) + } + } + methodChannelManager?.shuffleChanged(shuffleIsActive: isShuffleModeEnabled) + } + + func fillShuffledQueue() { + shuffledQueue.removeAll() + for index in shuffledIndices { + if index < fullQueue.count { + shuffledQueue.append(fullQueue[index]) + } } - - return smPlayer.items().firstIndex(of: currentItem) + } + + + func reorder(fromIndex: Int, toIndex: Int, positionsList: [[String: Int]]) { + var queue = isShuffleModeEnabled ? shuffledQueue : fullQueue + queue.insert(queue.remove(at: fromIndex), at: toIndex) + distributeItemsInRightQueue(currentQueue: queue) } func nextTrack() { @@ -106,23 +135,35 @@ public class SMPlayer : NSObject { historyQueue.append(currentItem) } smPlayer.advanceToNextItem() + seekToPosition(position: 0) setNowPlaying() insertIntoPlayerIfNeeded() + printStatus(from:"NEXT") } func previousTrack() { - //TESTE com pause - guard let lastHistoryItem = historyQueue.popLast() else { return /*smPlayer.seek(to: CMTime.zero) */} - let currentItem = smPlayer.currentItem - let lastItemInPlayer = smPlayer.items().last! - smPlayer.remove(lastItemInPlayer) - futureQueue.insert(lastItemInPlayer, at: 0) - smPlayer.insert(lastHistoryItem, after: currentItem!) + smPlayer.pause() + guard let lastHistoryItem = historyQueue.popLast() else { + seekToPosition(position: 0) + return + } + guard let currentItem = smPlayer.currentItem else { return} + guard let lastItemInPlayer = smPlayer.items().last else { return } + + if(currentItem != lastItemInPlayer) { + smPlayer.remove(lastItemInPlayer) + futureQueue.insert(lastItemInPlayer, at: 0) + } + + smPlayer.insert(lastHistoryItem, after: currentItem) smPlayer.advanceToNextItem() - smPlayer.insert(currentItem!, after: smPlayer.currentItem) - printStatus() + smPlayer.insert(currentItem, after: smPlayer.currentItem) + seekToPosition(position: 0) setNowPlaying() + insertIntoPlayerIfNeeded() + smPlayer.play() + printStatus(from:"previousTrack") } func setNowPlaying(){ @@ -136,36 +177,30 @@ public class SMPlayer : NSObject { for _ in 0.. PlaylistItem? { @@ -175,20 +210,40 @@ public class SMPlayer : NSObject { return currentItem.playlistItem } - func playFromQueue(position: Int, timePosition: Int, loadOnly: Bool) { - let allItems = currentQueue - smPlayer.removeAllItems() + private func distributeItemsInRightQueue(currentQueue: [AVPlayerItem], keepFirst: Bool = true, positionArg: Int = -1) { + guard currentQueue.count > 0 else { return } + + var position = positionArg + historyQueue.removeAll() futureQueue.removeAll() - for (index, item) in allItems.enumerated() { - if index < position { - historyQueue.append(item) - } else { - futureQueue.append(item) -// MessageBuffer.shared.sendUnique(item) + + + if(keepFirst){ + position = smPlayer.currentItem != nil ? currentQueue.firstIndex(of:smPlayer.currentItem!) ?? -1 : -1 + let itemsToRemove = smPlayer.items().dropFirst() + for item in itemsToRemove { + smPlayer.remove(item) + } + }else{ + smPlayer.removeAllItems() + } + + + for (index, item) in currentQueue.enumerated() { + if(index != position){ + if index < position { + historyQueue.append(item) + } else { + futureQueue.append(item) + } } } insertIntoPlayerIfNeeded() + } + + func playFromQueue(position: Int, timePosition: Int, loadOnly: Bool) { + distributeItemsInRightQueue(currentQueue:fullQueue, keepFirst: false, positionArg: position) if(loadOnly){ seekToPosition(position: timePosition) smPlayer.pause() @@ -213,11 +268,11 @@ public class SMPlayer : NSObject { } commandCenter.nextTrackCommand.addTarget {[self]event in - smPlayer.advanceToNextItem() + nextTrack() return .success } commandCenter.previousTrackCommand.addTarget {[self]event in - smPlayer.advanceToNextItem() + previousTrack() return .success } @@ -228,17 +283,20 @@ public class SMPlayer : NSObject { } } - func printStatus() { + func printStatus(from:String) { print("#printStatus #################################################") + print("#printStatus \(from) ") + print("#printStatus Current Index: \(String(describing: currentIndex))") + print("#printStatus ------------------------------------------") print("#printStatus History: \(historyQueue.count) items") for item in historyQueue { print("#printStatus History: \(String(describing: item.playlistItem?.title))") } print("#printStatus ------------------------------------------") print("#printStatus futureQueue Items: \(futureQueue.count) items") -// for item in futureQueue { -// print("#printStatus Upcoming: \(String(describing: item.playlistItem?.title))") -// } + for item in futureQueue { + print("#printStatus Upcoming: \(String(describing: item.playlistItem?.title))") + } print("#printStatus ------------------------------------------") print("#printStatus AVQueuePlayer items: \(smPlayer.items().count)") for item in smPlayer.items() { @@ -254,6 +312,7 @@ public class SMPlayer : NSObject { smPlayer.play() } } + extension AVPlayerItem { var playlistItem: PlaylistItem? { get { @@ -265,8 +324,6 @@ extension AVPlayerItem { } } - -// Extensão de AVPlayer para suporte a modo de repetição public extension AVQueuePlayer { private struct CustomProperties { static var repeatManager: UInt8 = 0 From 70662c2ed092cc9197082a4d174a6521caeba982 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 10 Oct 2024 16:16:18 -0300 Subject: [PATCH 50/70] fix observers --- packages/player/ios/Classes/Consts.swift | 2 +- .../ios/Classes/MethodChannelManager.swift | 28 ++- .../player/ios/Classes/PlayerPlugin.swift | 8 +- .../player/ios/Classes/PlaylistItem.swift | 6 +- .../player/ios/Classes/RepeatManager.swift | 84 -------- packages/player/ios/Classes/SMPlayer.swift | 186 +++++++++++------- .../ios/Classes/SMPlayerListeners.swift | 161 +++++++++------ 7 files changed, 248 insertions(+), 227 deletions(-) delete mode 100644 packages/player/ios/Classes/RepeatManager.swift diff --git a/packages/player/ios/Classes/Consts.swift b/packages/player/ios/Classes/Consts.swift index bf13f349..1ccd6801 100644 --- a/packages/player/ios/Classes/Consts.swift +++ b/packages/player/ios/Classes/Consts.swift @@ -44,7 +44,7 @@ let DURATION_ARGS = "duration" let STATE_ARGS = "state" let CURRENT_MEDIA_INDEX_ARGS = "CURRENT_MEDIA_INDEX" let ITS_AD_TIME = "its_ad_time" -let ERROR_ARGS = "error_args" +let ERROR_ARGS = "error" let QUEUE_ARGS = "queue_args" let EXTRAS_ARGS = "extras_args" let IS_FAVORITE_ARGS = "is_favorite_args" diff --git a/packages/player/ios/Classes/MethodChannelManager.swift b/packages/player/ios/Classes/MethodChannelManager.swift index b7f1dcfb..f34b56a0 100644 --- a/packages/player/ios/Classes/MethodChannelManager.swift +++ b/packages/player/ios/Classes/MethodChannelManager.swift @@ -18,34 +18,48 @@ public class MethodChannelManager:NSObject{ self.channel = channel } var tag = "MethodChannelManager" -// let methodChannel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger) - func notifyPositionChange( position: Double, duration: Double) { let args = MethodChannelManagerArgsBuilder() - .playerId(id:"TAG_ONEPLAYER") + .playerId(id:"smplayer") .position(position:Int(position)*1000) .duration(duration:Int(duration)*1000) -// .currentMediaIndex(currentMediaIndex:currentMediaIndex) .build() -// print("notifyPositionChange no tempo: \(position) segundos | duration: \(duration)") channel?.invokeMethod("audio.onCurrentPosition", arguments: args) } + func notifyNetworkStatus( + status:Bool) { + let args = MethodChannelManagerArgsBuilder() + .playerId(id:"smplayer") + .build() + channel?.invokeMethod("network.onChange", arguments: status ? "CONNECTED" : "DISCONNECTED") + } + func notifyPlayerStateChange(state: PlayerState) { + print("#CheckListeners - notifyPlayerStateChange \(state)") let args = MethodChannelManagerArgsBuilder() - .playerId(id:"TAG_ONEPLAYER") + .playerId(id:"smplayer") .state(state:state) .build() channel?.invokeMethod("state.change", arguments: args) } + func notifyError(error: String? = nil) { + let args = MethodChannelManagerArgsBuilder() + .playerId(id: "smplayer") + .state(state:PlayerState.error) + .error(error:error) + .build() + channel?.invokeMethod("state.change", arguments: args) + } + func currentMediaIndex(index: Int) { let args = MethodChannelManagerArgsBuilder() - .playerId(id:"TAG_ONEPLAYER") + .playerId(id:"smplayer") .currentIndex(index:index) .build() channel?.invokeMethod(SET_CURRENT_MEDIA_INDEX, arguments: args) diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index 5ac523ca..a59c06df 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -40,7 +40,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { } result(NSNumber(value: true)) case "next": - smPlayer?.nextTrack() + smPlayer?.nextTrack(from:"next call") result(NSNumber(value: 1)) case "previous": smPlayer?.previousTrack() @@ -67,6 +67,9 @@ public class PlayerPlugin: NSObject, FlutterPlugin { case "remove_all": smPlayer?.removeAll() result(NSNumber(value: true)) + case "repeat_mode": + smPlayer?.toggleRepeatMode() + result(NSNumber(value: 1)) case "reorder": if let args = call.arguments as? [String: Any], let oldIndex = args["oldIndex"] as? Int, @@ -114,7 +117,8 @@ public class PlayerPlugin: NSObject, FlutterPlugin { coverUrl: coverUrl as? String ?? "", fallbackUrl: fallbackUrl as? String ?? "", mediaId: id as! Int, - bigCoverUrl: bigCoverUrl as? String ?? "" + bigCoverUrl: bigCoverUrl as? String ?? "", + cookie: "" ) mediaList.append(media) } diff --git a/packages/player/ios/Classes/PlaylistItem.swift b/packages/player/ios/Classes/PlaylistItem.swift index 269fd577..ba18b52f 100644 --- a/packages/player/ios/Classes/PlaylistItem.swift +++ b/packages/player/ios/Classes/PlaylistItem.swift @@ -22,9 +22,11 @@ import AVFoundation @objc public var mediaId: Int @objc public var bigCoverUrl: String? + + @objc public var cookie: String? // @objc public var index: Int - @objc public init(albumId: String, albumName: String, title: String, artist: String, url: String, coverUrl: String,fallbackUrl: String, mediaId:Int, bigCoverUrl: String) { + @objc public init(albumId: String, albumName: String, title: String, artist: String, url: String, coverUrl: String,fallbackUrl: String, mediaId:Int, bigCoverUrl: String, cookie: String) { self.albumId = albumId self.albumName = albumName self.title = title @@ -36,6 +38,6 @@ import AVFoundation self.fallbackUrl = fallbackUrl self.mediaId = mediaId self.bigCoverUrl = bigCoverUrl -// self.index = index + self.cookie = cookie } } diff --git a/packages/player/ios/Classes/RepeatManager.swift b/packages/player/ios/Classes/RepeatManager.swift deleted file mode 100644 index a7ab70a6..00000000 --- a/packages/player/ios/Classes/RepeatManager.swift +++ /dev/null @@ -1,84 +0,0 @@ -import AVFoundation - -// Gerenciador de repetição -public final class RepeatManager: NSObject { - weak var player: AVQueuePlayer? - - var mode: AVQueuePlayer.RepeatMode = .REPEAT_MODE_OFF - - init(player: AVQueuePlayer) { - self.player = player - super.init() - - startObservingCurrentItem(of: player) - if let playerItem = player.currentItem { - startObservingNotifications(of: playerItem) - } - } - - deinit { - guard let player = player else { - return - } - - stopObservingCurrentItem(of: player) - if let playerItem = player.currentItem { - stopObservingNotifications(of: playerItem) - } - } - - func startObservingCurrentItem(of player: AVPlayer) { - player.addObserver(self, forKeyPath: "currentItem", options: [.old, .new], context: nil) - } - - func stopObservingCurrentItem(of player: AVPlayer) { - player.removeObserver(self, forKeyPath: "currentItem") - } - - func startObservingNotifications(of playerItem: AVPlayerItem) { - NotificationCenter.default.addObserver( - self, - selector: #selector(handleItemDidPlayToEnd(notification:)), - name: .AVPlayerItemDidPlayToEndTime, - object: playerItem - ) - } - - func stopObservingNotifications(of playerItem: AVPlayerItem) { - NotificationCenter.default.removeObserver( - self, - name: .AVPlayerItemDidPlayToEndTime, - object: playerItem - ) - } - - @objc func handleItemDidPlayToEnd(notification: Notification) { - guard let player = player, let currentItem = player.currentItem, currentItem == notification.object as? AVPlayerItem else { - return - } - - switch mode { - case .REPEAT_MODE_ALL: - player.advanceToNextItem() - player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) - player.insert(currentItem, after: nil) - case .REPEAT_MODE_ONE: - player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) - default: - break - } - } - - public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - if keyPath == "currentItem", let player = object as? AVPlayer { - if let oldItem = change?[.oldKey] as? AVPlayerItem { - stopObservingNotifications(of: oldItem) - } - if let newItem = change?[.newKey] as? AVPlayerItem { - startObservingNotifications(of: newItem) - } - } - } -} - - diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index c32f88ac..bbe4f218 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -3,7 +3,7 @@ import AVFoundation import MediaPlayer private var playlistItemKey: UInt8 = 0 - +var currentRepeatmode: AVQueuePlayer.RepeatMode = .REPEAT_MODE_OFF public class SMPlayer : NSObject { var methodChannelManager: MethodChannelManager? //Queue handle @@ -16,7 +16,8 @@ public class SMPlayer : NSObject { private var shuffledIndices: [Int] = [] private var isShuffleModeEnabled: Bool = false var shuffledQueue: [AVPlayerItem] = [] - + private var listeners: SMPlayerListeners? = nil + var fullQueue: [AVPlayerItem] { return historyQueue + smPlayer.items() + futureQueue } @@ -32,26 +33,54 @@ public class SMPlayer : NSObject { smPlayer = AVQueuePlayer() super.init() self.methodChannelManager = methodChannelManager - let listeners = SMPlayerListeners(playerItem: playerItem,smPlayer:smPlayer,methodChannelManager:methodChannelManager) - listeners.addObservers() - listeners.onMediaChanged = { - methodChannelManager?.currentMediaIndex(index: self.historyQueue.count) + listeners = SMPlayerListeners(smPlayer:smPlayer,methodChannelManager:methodChannelManager) + listeners?.addPlayerObservers() + listeners?.onMediaChanged = { + if(self.smPlayer.items().count > 0){ + if(self.smPlayer.currentItem != self.fullQueue.first && self.historyQueue.count > 0){ + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) + self.updateEndPlaybackObserver() + } + methodChannelManager?.currentMediaIndex(index: self.historyQueue.count) + self.listeners?.addItemsObservers() + } } setupNowPlayingInfoCenter() _ = AudioSessionManager.activeSession() - _ = RepeatManager(player:smPlayer) - NotificationCenter.default.addObserver(self, selector: #selector(itemDidFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: smPlayer.currentItem) - } func pause() { smPlayer.pause() } + func addEndPlaybackObserver() { + guard let currentItem = smPlayer.currentItem else { return } + NotificationCenter.default.addObserver( + self, + selector: #selector(itemDidFinishPlaying(_:)), + name: .AVPlayerItemDidPlayToEndTime, + object: currentItem + ) + } + func removeEndPlaybackObserver() { + if let currentItem = smPlayer.currentItem { + NotificationCenter.default.removeObserver( + self, + name: .AVPlayerItemDidPlayToEndTime, + object: currentItem + ) + } + } + + func updateEndPlaybackObserver() { + removeEndPlaybackObserver() + addEndPlaybackObserver() + } + func disableRepeatMode() { smPlayer.repeatMode = .REPEAT_MODE_OFF - methodChannelManager?.repeatModeChanged(repeatMode: smPlayer.repeatMode.hashValue) + methodChannelManager?.repeatModeChanged(repeatMode: smPlayer.repeatModeIndex) } func seek(position:Int){ @@ -68,7 +97,7 @@ public class SMPlayer : NSObject { case .REPEAT_MODE_ONE: smPlayer.repeatMode = .REPEAT_MODE_OFF } - methodChannelManager?.repeatModeChanged(repeatMode: smPlayer.repeatMode.hashValue) + methodChannelManager?.repeatModeChanged(repeatMode: smPlayer.repeatModeIndex) } @@ -86,11 +115,16 @@ public class SMPlayer : NSObject { guard let message = MessageBuffer.shared.receive() else { return } let isFirstBatch = self.smPlayer.items().count == 0 for media in message { - guard let url = URL(string: media.url!) else { continue } - let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie]] - let playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) - playerItem.playlistItem = media - futureQueue.append(playerItem) + if(media.url!.contains("https")){ + guard let url = URL(string: media.url!) else { continue } + let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie]] + playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) + }else{ + playerItem = AVPlayerItem(asset:AVAsset(url: NSURL(fileURLWithPath: media.url!) as URL)) + media.cookie = cookie + } + playerItem!.playlistItem = media + futureQueue.append(playerItem!) } insertIntoPlayerIfNeeded() if autoPlay && isFirstBatch { @@ -115,8 +149,8 @@ public class SMPlayer : NSObject { } func fillShuffledQueue() { - shuffledQueue.removeAll() - for index in shuffledIndices { + shuffledQueue.removeAll() + for index in shuffledIndices { if index < fullQueue.count { shuffledQueue.append(fullQueue[index]) } @@ -130,7 +164,8 @@ public class SMPlayer : NSObject { distributeItemsInRightQueue(currentQueue: queue) } - func nextTrack() { + func nextTrack(from:String) { + print("#print nextTrack \(from)") if let currentItem = smPlayer.currentItem { historyQueue.append(currentItem) } @@ -192,6 +227,9 @@ public class SMPlayer : NSObject { historyQueue.removeAll() futureQueue.removeAll() originalQueue.removeAll() + shuffledQueue.removeAll() + shuffledIndices.removeAll() + } func play(){ @@ -200,7 +238,7 @@ public class SMPlayer : NSObject { func seekToPosition(position:Int){ let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: 60000) - smPlayer.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero) + smPlayer.currentItem?.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: nil) } func getCurrentPlaylistItem() -> PlaylistItem? { @@ -212,13 +250,13 @@ public class SMPlayer : NSObject { private func distributeItemsInRightQueue(currentQueue: [AVPlayerItem], keepFirst: Bool = true, positionArg: Int = -1) { guard currentQueue.count > 0 else { return } - + if(!keepFirst){ + guard positionArg >= 0 else { return } + } var position = positionArg - historyQueue.removeAll() futureQueue.removeAll() - if(keepFirst){ position = smPlayer.currentItem != nil ? currentQueue.firstIndex(of:smPlayer.currentItem!) ?? -1 : -1 let itemsToRemove = smPlayer.items().dropFirst() @@ -227,9 +265,9 @@ public class SMPlayer : NSObject { } }else{ smPlayer.removeAllItems() + futureQueue.append(currentQueue[position]) } - for (index, item) in currentQueue.enumerated() { if(index != position){ if index < position { @@ -242,11 +280,14 @@ public class SMPlayer : NSObject { insertIntoPlayerIfNeeded() } - func playFromQueue(position: Int, timePosition: Int, loadOnly: Bool) { + func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false) { distributeItemsInRightQueue(currentQueue:fullQueue, keepFirst: false, positionArg: position) + methodChannelManager?.currentMediaIndex(index: self.historyQueue.count) + seekToPosition(position: timePosition) if(loadOnly){ - seekToPosition(position: timePosition) - smPlayer.pause() + pause() + }else{ + play() } } @@ -268,7 +309,7 @@ public class SMPlayer : NSObject { } commandCenter.nextTrackCommand.addTarget {[self]event in - nextTrack() + nextTrack(from: "commandCenter.nextTrackCommand") return .success } commandCenter.previousTrackCommand.addTarget {[self]event in @@ -284,32 +325,49 @@ public class SMPlayer : NSObject { } func printStatus(from:String) { - print("#printStatus #################################################") - print("#printStatus \(from) ") - print("#printStatus Current Index: \(String(describing: currentIndex))") - print("#printStatus ------------------------------------------") - print("#printStatus History: \(historyQueue.count) items") - for item in historyQueue { - print("#printStatus History: \(String(describing: item.playlistItem?.title))") - } - print("#printStatus ------------------------------------------") - print("#printStatus futureQueue Items: \(futureQueue.count) items") - for item in futureQueue { - print("#printStatus Upcoming: \(String(describing: item.playlistItem?.title))") - } - print("#printStatus ------------------------------------------") - print("#printStatus AVQueuePlayer items: \(smPlayer.items().count)") - for item in smPlayer.items() { - print("#printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") + //TODO: adicionar variavel de debug + if(true){ + print("#printStatus #################################################") + print("#printStatus \(from) ") + print("#printStatus Current Index: \(String(describing: currentIndex))") + print("#printStatus ------------------------------------------") + print("#printStatus History: \(historyQueue.count) items") + + for item in historyQueue { + print("#printStatus History: \(String(describing: item.playlistItem?.title))") + } + print("#printStatus ------------------------------------------") + print("#printStatus futureQueue Items: \(futureQueue.count) items") + + for item in futureQueue { + print("#printStatus Upcoming: \(String(describing: item.playlistItem?.title))") + } + print("#printStatus ------------------------------------------") + print("#printStatus AVQueuePlayer items: \(smPlayer.items().count)") + + for item in smPlayer.items() { + print("#printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") + } + print("#printStatus #################################################") } - print("#printStatus #################################################") } //override automatic next @objc func itemDidFinishPlaying(_ notification: Notification) { - smPlayer.pause() - nextTrack() - smPlayer.play() + pause() + switch smPlayer.repeatMode { + case .REPEAT_MODE_ALL: + if(smPlayer.currentItem == fullQueue.last){ + playFromQueue(position: 0) + break + } + nextTrack(from:"REPEAT_MODE_ALL") + case .REPEAT_MODE_ONE: + seekToPosition(position: 0) + case .REPEAT_MODE_OFF: + nextTrack(from: "REPEAT_MODE_OFF") + } + play() } } @@ -325,20 +383,7 @@ extension AVPlayerItem { } public extension AVQueuePlayer { - private struct CustomProperties { - static var repeatManager: UInt8 = 0 - } - - private var repeatManager: RepeatManager? { - get { - return objc_getAssociatedObject(self, &CustomProperties.repeatManager) as? RepeatManager - } - set(value) { - objc_setAssociatedObject(self, &CustomProperties.repeatManager, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } - - enum RepeatMode { + enum RepeatMode: Int,CaseIterable { case REPEAT_MODE_OFF case REPEAT_MODE_ONE case REPEAT_MODE_ALL @@ -346,23 +391,26 @@ public extension AVQueuePlayer { var repeatMode: RepeatMode { get { - return repeatManager?.mode ?? .REPEAT_MODE_OFF + return currentRepeatmode } set(mode) { - if repeatManager == nil { - repeatManager = RepeatManager(player: self ) - } - repeatManager?.mode = mode + + currentRepeatmode = mode switch mode { case .REPEAT_MODE_OFF: actionAtItemEnd = .none case .REPEAT_MODE_ALL: - actionAtItemEnd = .advance + actionAtItemEnd = .pause default: actionAtItemEnd = .pause } } + } + var repeatModeIndex: Int { + return RepeatMode.allCases.firstIndex(of: repeatMode) ?? -1 + } + } diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index e6d4eb93..dcd1af14 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -1,27 +1,17 @@ -// -// SMPlayerListeners.swift -// smplayer -// -// Created by Lucas Tonussi on 26/09/24. -// - import Foundation import AVFoundation public class SMPlayerListeners : NSObject { - var playerItem:AVPlayerItem? - let smPlayer:AVQueuePlayer + let smPlayer: AVQueuePlayer let methodChannelManager: MethodChannelManager? var onMediaChanged: (() -> Void)? - init(playerItem: AVPlayerItem?, smPlayer: AVQueuePlayer,methodChannelManager: MethodChannelManager?) { - self.playerItem = playerItem + init(smPlayer: AVQueuePlayer, methodChannelManager: MethodChannelManager?) { self.smPlayer = smPlayer self.methodChannelManager = methodChannelManager super.init() - - + addPlayerObservers() } private var mediaChange: NSKeyValueObservation? @@ -32,87 +22,134 @@ public class SMPlayerListeners : NSObject { private var notPlayingReason: NSKeyValueObservation? private var playback: NSKeyValueObservation? - func addObservers() { - let interval = CMTime(seconds: 0.5, - preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [self] time in - let position : Float64 = CMTimeGetSeconds(smPlayer.currentTime()); - if(smPlayer.currentItem != nil){ - let duration : Float64 = CMTimeGetSeconds(smPlayer.currentItem!.duration); - if(position < duration){ - methodChannelManager?.notifyPositionChange(position: position, duration: duration) - NowPlayingCenter.update(item: smPlayer.currentItem?.playlistItem, rate: 1.0, position: position, duration: duration) + + func addItemsObservers() { + removeItemObservers() + guard let currentItem = smPlayer.currentItem else { return } + statusChange = currentItem.observe(\.status, options: [.new, .old]) { (playerItem, change) in + if playerItem.status == .readyToPlay { + print("#Listeners - readyToPlay") + } else if playerItem.status == .failed { + if let error = playerItem.error { + print("#Listeners notifyError \(error.localizedDescription)") + if let fallbackUrl = playerItem.playlistItem?.fallbackUrl { + let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": playerItem.playlistItem?.cookie ?? ""]] + let fallbackItem = AVPlayerItem(asset: AVURLAsset(url: URL(string: fallbackUrl)! , options: assetOptions)) + + self.smPlayer.replaceCurrentItem(with: fallbackItem) + } else { + self.methodChannelManager?.notifyError(error: "NO FALLBACK") + } + } else { + self.methodChannelManager?.notifyError(error: "UNKNOW ERROR") } } } + + loading = currentItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old]) { [weak self] (new, old) in + guard let self = self else { return } + print("#Listeners - observer - loading") + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + } + loaded = currentItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { (player, _) in + print("#Listeners - observer - loaded") + } + } + + + + func addPlayerObservers() { mediaChange = smPlayer.observe(\.currentItem, options: [.new, .old]) { [weak self] (player, change) in guard let self = self else { return } + let oldItemExists = change.oldValue != nil + print("onMediaChanged: \(oldItemExists)") if let newItem = change.newValue, newItem != change.oldValue { - onMediaChanged?() - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) + self.onMediaChanged?() + self.addItemsObservers() } } - statusChange = smPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { - (playerItem, change) in - if playerItem.status == .readyToPlay { - print("readyToPlay") - } else if playerItem.status == .failed { - print("failed") + let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in + guard let self = self else { return } + let position: Float64 = CMTimeGetSeconds(self.smPlayer.currentTime()) + if let currentItem = self.smPlayer.currentItem { + let duration: Float64 = CMTimeGetSeconds(currentItem.duration) + if position < duration { + self.methodChannelManager?.notifyPositionChange(position: position, duration: duration) + NowPlayingCenter.update(item: currentItem.playlistItem, rate: 1.0, position: position, duration: duration) + } } - }) - - loading = smPlayer.currentItem?.observe(\.isPlaybackBufferEmpty, options: [.new,.old]) { [self] - (new, old) in - print("observer - loading") - methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) } - loaded = smPlayer.currentItem?.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { - (player, _) in - - print("observer - loaded") - } - - - notPlayingReason = smPlayer.observe(\.reasonForWaitingToPlay, options: [.new], changeHandler: { [self] - (playerItem, change) in - switch (smPlayer.reasonForWaitingToPlay) { - case AVPlayer.WaitingReason.evaluatingBufferingRate: - print("evaluatingBufferingRate") - case AVPlayer.WaitingReason.toMinimizeStalls: - print("toMinimizeStalls") - case AVPlayer.WaitingReason.noItemToPlay: - print("noItemToPlay") + notPlayingReason = smPlayer.observe(\.reasonForWaitingToPlay, options: [.new]) { (playerItem, change) in + switch self.smPlayer.reasonForWaitingToPlay { + case .evaluatingBufferingRate: + print("#Listeners reasonForWaitingToPlay - evaluatingBufferingRate") + case .toMinimizeStalls: + print("#Listeners reasonForWaitingToPlay - toMinimizeStalls") + case .noItemToPlay: + print("#Listeners reasonForWaitingToPlay - noItemToPlay") default: - print("default") + print("#Listeners reasonForWaitingToPlay - default") } - }) + } playback = smPlayer.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] (player, change) in guard let self = self else { return } - switch player.timeControlStatus { case .playing: self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.playing) + print("#Listeners - Playing") case .paused: self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.paused) + print("#Listeners - Paused") case .waitingToPlayAtSpecifiedRate: self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + print("#Listeners - Buffering") @unknown default: break } } + } + + func removeItemObservers() { + statusChange?.invalidate() + loading?.invalidate() + loaded?.invalidate() - smPlayer.addObserver(self, forKeyPath: "error", options: [.old, .new], context: nil) + statusChange = nil + loading = nil + loaded = nil + + removeErrorObserver() } - public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - if keyPath == "error", let change = change, let error = change[.newKey] as? Error { - if (error as NSError).code == AVError.contentIsNotAuthorized.rawValue { - print("Erro HTTP: contentIsNotAuthorized") - } + + func removeErrorObserver() { + if let currentItem = smPlayer.currentItem { + NotificationCenter.default.removeObserver( + self, + name: .AVPlayerItemFailedToPlayToEndTime, + object: currentItem + ) } } + + + func removePlayerObservers() { + notPlayingReason?.invalidate() + playback?.invalidate() + mediaChange?.invalidate() + + mediaChange = nil + notPlayingReason = nil + playback = nil + } + + deinit { + removePlayerObservers() + removeItemObservers() + } } From ced17b5b7720200a0e2e3f5df94754a7c313da0b Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 10 Oct 2024 17:25:11 -0300 Subject: [PATCH 51/70] improve example and fix isar --- packages/player/example/ios/Runner/Info.plist | 23 +- packages/player/example/lib/sm_player.dart | 355 +-- packages/player/ios/Classes/PlayerPlugin.m | 2006 ----------------- packages/player/lib/src/player.dart | 19 +- packages/player/lib/src/queue.dart | 5 +- packages/player/pubspec.yaml | 1 + 6 files changed, 243 insertions(+), 2166 deletions(-) delete mode 100644 packages/player/ios/Classes/PlayerPlugin.m diff --git a/packages/player/example/ios/Runner/Info.plist b/packages/player/example/ios/Runner/Info.plist index 9c35c88c..12191d1e 100644 --- a/packages/player/example/ios/Runner/Info.plist +++ b/packages/player/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -22,6 +24,18 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + audio + processing + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,14 +55,5 @@ UIViewControllerBasedStatusBarAppearance - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index fc76fca1..640d17a6 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -8,6 +8,8 @@ import 'package:flutter/services.dart'; import 'package:smplayer_example/app_colors.dart'; import 'package:smplayer_example/ui_data.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:collection/collection.dart'; +import 'package:file_picker/file_picker.dart'; class SMPlayer extends StatefulWidget { SMPlayer({ @@ -18,6 +20,84 @@ class SMPlayer extends StatefulWidget { State createState() => _SMPlayerState(); } +var media1 = Media( + id: 1, + albumTitle: "Album", + albumId: 1, + name: "Track 1", + url: + "https://android.suamusica.com.br/373377/2238511/03+Solteiro+Largado.mp3", + coverUrl: + "https://images.suamusica.com.br/5hxcfuN3q0lXbSiWXaEwgRS55gQ=/240x240/373377/2238511/cd_cover.jpeg", + bigCoverUrl: + "https://images.suamusica.com.br/5hxcfuN3q0lXbSiWXaEwgRS55gQ=/240x240/373377/2238511/cd_cover.jpeg", + author: "Xand Avião", + isLocal: false, + isVerified: true, + shareUrl: "", + isSpot: false, + ownerId: 0, + playlistId: 0, +); + +var media3 = Media( + id: 1, + albumTitle: "Album unsigned", + albumId: 1, + name: "Track unsigned", + url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", + coverUrl: "https://picsum.photos/500/500", + bigCoverUrl: "https://picsum.photos/500/500", + author: "Xand Avião", + fallbackUrl: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", + isLocal: false, + isVerified: true, + shareUrl: "", + isSpot: false, + ownerId: 0, + playlistId: 0, +); + +var media4 = Media( + id: 1, + albumTitle: "É o Grelo", + albumId: 1, + name: "01 - VIDA LOK4 part.1", + url: + "https://ios-stream.suamusica.com.br/61523203/4435593/stream/01+-+VIDA+LOK4+part.1.m3u8", + coverUrl: + "https://images.suamusica.com.br/FTpOBQiRWVspoErSnNQgZxhou40=/500x500/filters:format(webp)/61523203/4435593/cd_cover.jpeg", + bigCoverUrl: + "https://images.suamusica.com.br/FTpOBQiRWVspoErSnNQgZxhou40=/500x500/filters:format(webp)/61523203/4435593/cd_cover.jpeg", + author: "É o Grelo", + fallbackUrl: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", + isLocal: false, + isVerified: true, + shareUrl: "", + isSpot: false, + ownerId: 0, + playlistId: 0, +); + +var media2 = Media( + id: 2, + albumTitle: "Album", + albumId: 1, + name: "Track 2", + url: "https://android.suamusica.com.br/373377/2238511/02+O+Bebe.mp3", + coverUrl: + "https://images.suamusica.com.br/5hxcfuN3q0lXbSiWXaEwgRS55gQ=/240x240/373377/2238511/cd_cover.jpeg", + bigCoverUrl: + "https://images.suamusica.com.br/5hxcfuN3q0lXbSiWXaEwgRS55gQ=/240x240/373377/2238511/cd_cover.jpeg", + author: "Xand Avião", + isLocal: false, + isVerified: true, + shareUrl: "", + isSpot: false, + ownerId: 0, + playlistId: 0, +); + class _SMPlayerState extends State { late Player _player; Media? currentMedia; @@ -90,65 +170,13 @@ class _SMPlayerState extends State { } }); - var media1 = Media( - id: 1, - albumTitle: "Album", - albumId: 1, - name: "Track 1", - url: - "https://android.suamusica.com.br/373377/2238511/03+Solteiro+Largado.mp3", - coverUrl: - "https://images.suamusica.com.br/5hxcfuN3q0lXbSiWXaEwgRS55gQ=/240x240/373377/2238511/cd_cover.jpeg", - bigCoverUrl: - "https://images.suamusica.com.br/5hxcfuN3q0lXbSiWXaEwgRS55gQ=/240x240/373377/2238511/cd_cover.jpeg", - author: "Xand Avião", - isLocal: false, - isVerified: true, - shareUrl: "", - isSpot: false, - ownerId: 0, - playlistId: 0, - ); + final listOfMedias = []; - var media3 = Media( - id: 1, - albumTitle: "Album unsigned", - albumId: 1, - name: "Track unsigned", - url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", - coverUrl: "https://picsum.photos/500/500", - bigCoverUrl: "https://picsum.photos/500/500", - author: "Xand Avião", - fallbackUrl: - "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", - isLocal: false, - isVerified: true, - shareUrl: "", - isSpot: false, - ownerId: 0, - playlistId: 0, - ); - - var media2 = Media( - id: 2, - albumTitle: "Album", - albumId: 1, - name: "Track 2", - url: "https://android.suamusica.com.br/373377/2238511/02+O+Bebe.mp3", - coverUrl: - "https://images.suamusica.com.br/5hxcfuN3q0lXbSiWXaEwgRS55gQ=/240x240/373377/2238511/cd_cover.jpeg", - bigCoverUrl: - "https://images.suamusica.com.br/5hxcfuN3q0lXbSiWXaEwgRS55gQ=/240x240/373377/2238511/cd_cover.jpeg", - author: "Xand Avião", - isLocal: false, - isVerified: true, - shareUrl: "", - isSpot: false, - ownerId: 0, - playlistId: 0, - ); + // for (var i = 0; i < 250; i++) { + listOfMedias.addAll([media1, media2, media3, media4]); + // } - player.enqueueAll([media1, media2, media3], autoPlay: true); + player.enqueueAll(listOfMedias, autoPlay: true); if (!mounted) return; @@ -269,18 +297,34 @@ class _SMPlayerState extends State { } } - _changeRepeatMode() { - if (_player.repeatMode == RepeatMode.REPEAT_MODE_OFF) { - setState(() { - _player.repeatMode = RepeatMode.REPEAT_MODE_ALL; - }); - } else if (_player.repeatMode == RepeatMode.REPEAT_MODE_ALL) { - setState(() { - _player.repeatMode = RepeatMode.REPEAT_MODE_ONE; - }); - } else { + Future pickLocalFile() async { + FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: true, + type: FileType.custom, + allowedExtensions: ['mp3'], + ); + + if (result != null) { + String? filePath = result.files.single.path; + var localMedia = Media( + id: DateTime.now().millisecondsSinceEpoch, // Unique ID + albumTitle: "Local Album", + albumId: 0, + name: result.files.single.name, + url: filePath ?? "", + coverUrl: "", // You can set a default cover URL or leave it empty + bigCoverUrl: "", + author: "Local Author", + isLocal: true, + isVerified: false, + shareUrl: "", + isSpot: false, + ownerId: 0, + playlistId: 0, + ); + setState(() { - _player.repeatMode = RepeatMode.REPEAT_MODE_OFF; + _player.enqueueAll([localMedia], autoPlay: true); }); } } @@ -294,48 +338,79 @@ class _SMPlayerState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - Stack(children: [ - Wrap( - direction: Axis.horizontal, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: EdgeInsets.only(left: 20.0), - child: - Text(positionText, style: TextStyle(fontSize: 14.0)), - ), - Padding( - padding: EdgeInsets.only(right: 20.0), - child: - Text(durationText, style: TextStyle(fontSize: 14.0)), - ) - ], - ), - ], - ), - Container( + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.delete), + onPressed: () { + _player.removeAll(); + }, + tooltip: 'Remove all', + ), + IconButton( + icon: Icon(Icons.add), + onPressed: () { + _player.enqueueAll( + [media1, media2, media3, media4], + autoPlay: true, + ); + }, + tooltip: 'Add media', + ), + IconButton( + icon: Icon(Icons.folder), + onPressed: pickLocalFile, + tooltip: 'Add local file', + ), + ], + ), + Stack( + children: [ + Wrap( + direction: Axis.horizontal, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: EdgeInsets.only(left: 20.0), + child: Text(positionText, + style: TextStyle(fontSize: 14.0)), + ), + Padding( + padding: EdgeInsets.only(right: 20.0), + child: Text(durationText, + style: TextStyle(fontSize: 14.0)), + ) + ], + ), + ], + ), + Container( width: double.infinity, margin: EdgeInsets.only(top: 5.0), child: SliderTheme( - data: SliderTheme.of(context).copyWith( - trackHeight: 2.0, - thumbShape: - const RoundSliderThumbShape(enabledThumbRadius: 7.0), - showValueIndicator: ShowValueIndicator.always, - ), - child: Slider( - activeColor: AppColors.redPink, - inactiveColor: AppColors.inactiveColor, - min: 0.0, - max: duration.inMilliseconds.toDouble(), - value: position.inMilliseconds.toDouble(), - onChanged: (double value) { - seek(value); - }, - ))), - ]), + data: SliderTheme.of(context).copyWith( + trackHeight: 2.0, + thumbShape: + const RoundSliderThumbShape(enabledThumbRadius: 7.0), + showValueIndicator: ShowValueIndicator.always, + ), + child: Slider( + activeColor: AppColors.redPink, + inactiveColor: AppColors.inactiveColor, + min: 0.0, + max: duration.inMilliseconds.toDouble(), + value: position.inMilliseconds.toDouble(), + onChanged: (double value) { + seek(value); + }, + ), + ), + ), + ], + ), Row( children: [ Container( @@ -359,14 +434,16 @@ class _SMPlayerState extends State { Container( margin: EdgeInsets.only(left: 8, right: 8), child: Material( - borderRadius: BorderRadius.circular(40.0), - clipBehavior: Clip.hardEdge, - child: IconButton( - onPressed: previous, - iconSize: 40, - icon: Container( - child: SvgPicture.asset(UIData.btPlayerPrevious), - ))), + borderRadius: BorderRadius.circular(40.0), + clipBehavior: Clip.hardEdge, + child: IconButton( + onPressed: previous, + iconSize: 40, + icon: Container( + child: SvgPicture.asset(UIData.btPlayerPrevious), + ), + ), + ), ), Material( borderRadius: BorderRadius.circular(58.0), @@ -401,7 +478,7 @@ class _SMPlayerState extends State { iconSize: 25, icon: SvgPicture.asset(UIData.btPlayerRepeat, color: _repeatModeToColor()), - onPressed: _changeRepeatMode, + onPressed: _player.toggleRepeatMode, )), ), ], @@ -410,23 +487,37 @@ class _SMPlayerState extends State { Text(mediaLabel), SizedBox(height: 30), Expanded( - child: SizedBox( - height: 200, - child: ListView.separated( - padding: const EdgeInsets.all(8.0), - itemCount: _player.items.length, - itemBuilder: (BuildContext context, int index) { - var media = _player.items[index]; - return Container( - height: 50, - color: Colors.blue[colorCodes[index % 3]], - child: Center(child: Text('${media.id} - ${media.name}')), - ); - }, - separatorBuilder: (BuildContext context, int index) => - const Divider(), + child: SizedBox( + height: 200, + child: ReorderableListView( + onReorder: (int oldIndex, int newIndex) { + if (newIndex > _player.items.length) { + newIndex = _player.items.length; + } + if (oldIndex < newIndex) { + newIndex--; + } + _player.reorder(oldIndex, newIndex); + }, + children: _player.items + .mapIndexed( + (index, media) => GestureDetector( + key: Key('queueItemWidgetKey$index'), + child: Container( + height: 50, + color: Colors.blue[colorCodes[index % 3]], + child: Center( + child: Text('${media.id} - ${media.name}')), + ), + onTap: () { + _player.playFromQueue(index); + }, + ), + ) + .toList(), + ), ), - )) + ) ], ), ); diff --git a/packages/player/ios/Classes/PlayerPlugin.m b/packages/player/ios/Classes/PlayerPlugin.m deleted file mode 100644 index f73b1d9b..00000000 --- a/packages/player/ios/Classes/PlayerPlugin.m +++ /dev/null @@ -1,2006 +0,0 @@ -#if __has_include() -#import -#else -// Support project import fallback if the generated compatibility header -// is not copied when this plugin is created as a library. -// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 -#import "smplayer-Swift.h" -#endif - -#import "PlayerPlugin.h" -#import "NSString+MD5.h" - -#import -#import -#import -#import -#import -#include - -@import AFNetworking; - -static NSString *const CHANNEL_NAME = @"suamusica.com.br/player"; -static NSString *redirectScheme = @"rdtp"; -static NSString *customPlaylistScheme = @"cplp"; -static NSString *customKeyScheme = @"ckey"; -static NSString *httpsScheme = @"https"; -static NSString *m3u8Ext = @".m3u8"; -static NSString *mp3Ext = @".mp3"; -static NSString *extInfo = @"#EXTINF:"; -static int redirectErrorCode = 302; -static int badRequestErrorCode = 400; - -static int const STATE_IDLE = 0; -static int const STATE_BUFFERING = 1; -static int const STATE_PLAYING = 2; -static int const STATE_PAUSED = 3; -static int const STATE_STOPPED = 4; -static int const STATE_COMPLETED = 5; -static int const STATE_ERROR = 6; -static int const STATE_SEEK_END = 7; -static int const STATE_BUFFER_EMPTY = 8; - -static bool loadOnly = false; - -static int Ok = 1; -static int NotOk = -1; - -static int const PLAYER_ERROR_FAILED = 0; -static int const PLAYER_ERROR_UNKNOWN = 1; -static int const PLAYER_ERROR_UNDEFINED = 2; -static int const PLAYER_ERROR_FAILED_TO_PLAY = 3; -static int const PLAYER_ERROR_FAILED_TO_PLAY_ERROR = 4; -static int const PLAYER_ERROR_NETWORK_ERROR = 5; - -static NSMutableDictionary * players; -static NSMutableDictionary * playersCurrentItem; - -NSString *DEFAULT_COVER = @"https://images.suamusica.com.br/gaMy5pP78bm6VZhPZCs4vw0TdEw=/500x500/imgs/cd_cover.png"; - -NSString *MINUTES_OF_SILENCE = @""; - -BOOL notifiedBufferEmptyWithNoConnection = false; - -@interface PlayerPlugin() --(int) pause: (NSString *) playerId; --(void) stop: (NSString *) playerId; --(int) seek: (NSString *) playerId time: (CMTime) time; --(void) onSoundComplete: (NSString *) playerId; --(void) updateDuration: (NSString *) playerId; --(void) onTimeInterval: (NSString *) playerId time: (CMTime) time; -@end - -@implementation PlayerPlugin { - FlutterResult _result; -} - -typedef void (^VoidCallback)(NSString * playerId); - -NSMutableSet *timeobservers; -FlutterMethodChannel *_channel_player = nil; -PlayerPlugin* instance = nil; -NSString* _playerId = nil; -BOOL alreadyInAudioSession = false; -BOOL isLoadingComplete = false; -AVAssetResourceLoadingRequest* currentResourceLoadingRequest = nil; -AVAssetResourceLoader* currentResourceLoader = nil; -dispatch_queue_t serialQueue = nil; -dispatch_queue_t playerQueue = nil; - -NSString* latestUrl = nil; -bool latestIsLocal = NO; -NSString* latestCookie = nil; -NSString* latestPlayerId = nil; -VoidCallback latestOnReady = nil; -AVPlayerItem* latestPlayerItemObserved = nil; -id playId = nil; -id pauseId = nil; -id nextTrackId = nil; -id previousTrackId = nil; -id togglePlayPauseId = nil; -id seekForwardId = nil; -id seekBackwardId = nil; -id changePlaybackPositionId = nil; -BOOL isConnected = true; -BOOL alreadyhasEnded = false; -BOOL shouldAutoStart = false; -BOOL stopTryingToReconnect = false; -NSString *lastName = nil; -NSString *lastAuthor = nil; -NSString *lastUrl = nil; -NSString *lastCoverUrl = nil; -NSString *lastCookie = nil; -float lastVolume = 1.0; -CMTime lastTime; -BOOL lastRespectSilence; - -BOOL shallSendEvents = true; -int wasPlayingBeforeVoiceSearch = -1; - -NSMutableArray *playlistItems = nil; - -PlaylistItem *currentItem = nil; - -+ (void)registerWithRegistrar:(NSObject*)registrar { - @synchronized(self) { - if (instance == nil) { - instance = [[PlayerPlugin alloc] init]; - FlutterMethodChannel* channel = [FlutterMethodChannel - methodChannelWithName:CHANNEL_NAME - binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; - [PlayerPlugin saveDefaultCover:registrar]; - _channel_player = channel; - - NSString* minutesOfSilenceKey = [registrar lookupKeyForAsset:@"assets/30-minutes-of-silence.mp3"]; - MINUTES_OF_SILENCE = [[NSBundle mainBundle] pathForResource:minutesOfSilenceKey ofType:nil]; - } - } -} - -+(void) saveDefaultCover:(NSObject*)registrar { - NSString *defaultCoverAssetKey = [registrar lookupKeyForAsset:@"assets/cd_cover.png"]; - NSString *defaultCoverPath = [[NSBundle mainBundle] pathForResource:defaultCoverAssetKey ofType:nil]; - [[CoverCenter shared] saveDefaultCoverWithPath:defaultCoverPath]; -} - -- (id)init { - NSLog(@"Player: INIT!"); - - self = [super init]; - if (self) { - serialQueue = dispatch_queue_create("com.suamusica.player.queue", DISPATCH_QUEUE_SERIAL); - playerQueue = dispatch_queue_create("com.suamusica.player.playerQueue", DISPATCH_QUEUE_SERIAL); - players = [[NSMutableDictionary alloc] init]; - playersCurrentItem = [[NSMutableDictionary alloc] init]; - [self configureRemoteCommandCenter]; - [self configureReachabilityCheck]; - [ScreenCenter addNotificationObservers]; - } - return self; -} - --(void)configureReachabilityCheck { - [[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { - NSLog(@"Reachability: %@", AFStringFromNetworkReachabilityStatus(status)); - - if (_playerId == nil || players == nil) { - return; - } - - NSString *networkStatus = @"CONNECTED"; - - switch (status) { - case AFNetworkReachabilityStatusUnknown: - case AFNetworkReachabilityStatusNotReachable: - { - isConnected = false; - networkStatus = @"DISCONNECTED"; - break; - } - case AFNetworkReachabilityStatusReachableViaWWAN: - case AFNetworkReachabilityStatusReachableViaWiFi: - isConnected = true; - NSMutableDictionary * playerInfo = players[_playerId]; - networkStatus = @"CONNECTED"; - if ([playerInfo[@"failedToStartPlaying"] boolValue]) { - // we did not even manage to start playing - // or in this case download the .m3u8 file - // so let's try everything again - [self play:_playerId name:lastName author:lastAuthor url:lastUrl coverUrl:lastCoverUrl cookie:lastCookie isLocal:false volume:lastVolume time:lastTime isNotification:lastRespectSilence]; - } else if (stopTryingToReconnect) { - // we manage to start playing - // the AVPlayer retried several times - // but it stoped - [self play:_playerId name:lastName author:lastAuthor url:lastUrl coverUrl:lastCoverUrl cookie:lastCookie isLocal:false volume:lastVolume time:lastTime isNotification:lastRespectSilence]; - } - break; - } - - if (shallSendEvents) { - [_channel_player invokeMethod:@"network.onChange" arguments:@{@"playerId": _playerId, @"status": networkStatus}]; - } - }]; - - [[AFNetworkReachabilityManager sharedManager] startMonitoring]; -} - --(void)configureRemoteCommandCenter { - NSLog(@"Player: MPRemote: Enabling Remote Command Center..."); - // TODO: Review this - // For now I will keep it disabled - //if ([ScreenCenter isUnlocked] && false) { - // NSLog(@"Player: Ending Remote Controel Events"); - // dispatch_async(dispatch_get_main_queue(), ^{ - // [[UIApplication sharedApplication] endReceivingRemoteControlEvents]; - // }); - // - //} - NSLog(@"Player: Starting Remote Controel Events"); - dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; - }); - MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; - - if (playId != nil) { - [commandCenter.playCommand removeTarget:playId]; - } - playId = [commandCenter.playCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - NSLog(@"Player: Remote Command Play: START"); - if (_playerId != nil) { - NSMutableDictionary * playerInfo = players[_playerId]; - if ([playerInfo[@"areNotificationCommandsEnabled"] boolValue]) { - NSLog(@"Player: Remote Command Play: Enabled"); - [self resume:_playerId]; - int state = STATE_PLAYING; - [self notifyStateChange:_playerId state:state overrideBlock:true]; - [_channel_player invokeMethod:@"commandCenter.onPlay" arguments:@{@"playerId": _playerId}]; - } else { - NSLog(@"Player: Remote Command Play: Disabled"); - } - } - NSLog(@"Player: Remote Command Play: END"); - return MPRemoteCommandHandlerStatusSuccess; - }]; - commandCenter.playCommand.enabled = TRUE; - if (pauseId != nil) { - [commandCenter.pauseCommand removeTarget:pauseId]; - } - pauseId = [commandCenter.pauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - NSLog(@"Player: Remote Command Pause: START"); - if (_playerId != nil) { - NSMutableDictionary * playerInfo = players[_playerId]; - if ([playerInfo[@"areNotificationCommandsEnabled"] boolValue]) { - NSLog(@"Player: Remote Command Pause: Enabled"); - [self pause:_playerId]; - int state = STATE_PAUSED; - [self notifyStateChange:_playerId state:state overrideBlock:true]; - [_channel_player invokeMethod:@"commandCenter.onPause" arguments:@{@"playerId": _playerId}]; - } else { - NSLog(@"Player: Remote Command Pause: Disabled"); - } - } - NSLog(@"Player: Remote Command Pause: END"); - return MPRemoteCommandHandlerStatusSuccess; - }]; - commandCenter.pauseCommand.enabled = TRUE; - if (togglePlayPauseId != nil) { - [commandCenter.togglePlayPauseCommand removeTarget:togglePlayPauseId]; - } - togglePlayPauseId = [commandCenter.togglePlayPauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - NSLog(@"Player: Remote Command TooglePlayPause: START"); - if (_playerId != nil) { - NSMutableDictionary * playerInfo = players[_playerId]; - if ([playerInfo[@"areNotificationCommandsEnabled"] boolValue]) { - AVQueuePlayer *player = playerInfo[@"player"]; - if (player.rate == 0.0) { - [self resume:_playerId]; - } else { - [self pause:_playerId]; - } - } else { - NSLog(@"Player: Remote Command TooglePlayPause: Disabled"); - } - } - NSLog(@"Player: Remote Command TooglePlayPause: END"); - return MPRemoteCommandHandlerStatusSuccess; - }]; - commandCenter.togglePlayPauseCommand.enabled = TRUE; - if (nextTrackId != nil) { - [commandCenter.nextTrackCommand removeTarget:nextTrackId]; - } - nextTrackId = [commandCenter.nextTrackCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - NSLog(@"Player: Remote Command Next: START"); - if (_playerId != nil) { - NSMutableDictionary * playerInfo = players[_playerId]; - if ([playerInfo[@"areNotificationCommandsEnabled"] boolValue]) { - NSLog(@"Player: Remote Command Next: Enabled"); - [_channel_player invokeMethod:@"commandCenter.onNext" arguments:@{@"playerId": _playerId}]; - } else { - NSLog(@"Player: Remote Command Next: Disabled"); - } - } - NSLog(@"Player: Remote Command Next: END"); - return MPRemoteCommandHandlerStatusSuccess; - }]; - commandCenter.nextTrackCommand.enabled = TRUE; - if (previousTrackId != nil) { - [commandCenter.previousTrackCommand removeTarget:previousTrackId]; - } - previousTrackId =[commandCenter.previousTrackCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - NSLog(@"Player: Remote Command Previous: START"); - if (_playerId != nil) { - NSMutableDictionary * playerInfo = players[_playerId]; - if ([playerInfo[@"areNotificationCommandsEnabled"] boolValue]) { - NSLog(@"Player: Remote Command Previous: Enabled"); - [_channel_player invokeMethod:@"commandCenter.onPrevious" arguments:@{@"playerId": _playerId}]; - } else { - NSLog(@"Player: Remote Command Previous: Disabled"); - } - } - NSLog(@"Player: Remote Command Previous: END"); - return MPRemoteCommandHandlerStatusSuccess; - }]; - commandCenter.previousTrackCommand.enabled = TRUE; - - if (@available(iOS 15.0, *)) { - if (seekForwardId != nil) { - [commandCenter.seekForwardCommand removeTarget:seekForwardId]; - } - seekForwardId = [commandCenter.seekForwardCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - NSLog(@"Player: Remote Command SeekForward: START"); - if (_playerId != nil) { - NSMutableDictionary * playerInfo = players[_playerId]; - if ([playerInfo[@"areNotificationCommandsEnabled"] boolValue]) { - NSLog(@"Player: Remote Command SeekForward: Enabled"); - [self seek:_playerId time:CMTimeMakeWithSeconds(30, NSEC_PER_SEC)]; - } else { - NSLog(@"Player: Remote Command SeekForward: Disabled"); - } - } - NSLog(@"Player: Remote Command SeekForward: END"); - return MPRemoteCommandHandlerStatusSuccess; - }]; - commandCenter.seekForwardCommand.enabled = TRUE; - - if (seekBackwardId != nil) { - [commandCenter.seekBackwardCommand removeTarget:seekBackwardId]; - } - seekBackwardId = [commandCenter.seekBackwardCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - NSLog(@"Player: Remote Command SeekBackward: START"); - if (_playerId != nil) { - NSMutableDictionary * playerInfo = players[_playerId]; - if ([playerInfo[@"areNotificationCommandsEnabled"] boolValue]) { - NSLog(@"Player: Remote Command SeekBackward: Enabled"); - [self seek:_playerId time:CMTimeMakeWithSeconds(-30, NSEC_PER_SEC)]; - } else { - NSLog(@"Player: Remote Command SeekBackward: Disabled"); - } - } - NSLog(@"Player: Remote Command SeekBackward: END"); - return MPRemoteCommandHandlerStatusSuccess; - }]; - commandCenter.seekBackwardCommand.enabled = TRUE; - - if (changePlaybackPositionId != nil) { - [commandCenter.changePlaybackPositionCommand removeTarget:changePlaybackPositionId]; - } - changePlaybackPositionId = [commandCenter.changePlaybackPositionCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - NSLog(@"Player: Remote Command ChangePlaybackPosition: START"); - if (_playerId != nil) { - NSMutableDictionary * playerInfo = players[_playerId]; - if ([playerInfo[@"areNotificationCommandsEnabled"] boolValue]) { - NSLog(@"Player: Remote Command ChangePlaybackPosition: Enabled"); - MPChangePlaybackPositionCommandEvent * playbackEvent = (MPChangePlaybackPositionCommandEvent *)event; - [self seek:_playerId time:CMTimeMakeWithSeconds(playbackEvent.positionTime, NSEC_PER_SEC)]; - } else { - NSLog(@"Player: Remote Command ChangePlaybackPosition: Disabled"); - } - } - NSLog(@"Player: Remote Command ChangePlaybackPosition: END"); - return MPRemoteCommandHandlerStatusSuccess; - }]; - commandCenter.changePlaybackPositionCommand.enabled = TRUE; - } - - NSLog(@"Player: MPRemote: Enabled Remote Command Center! Done!"); -} - --(void)disableRemoteCommandCenter:(NSString *) playerId { - NSLog(@"Player: MPRemote: Disabling Remote Command Center..."); - NSMutableDictionary * playerInfo = players[playerId]; - [playerInfo setObject:@false forKey:@"isPlaying"]; - NSError *error; - [AudioSessionManager inactivateSession]; - [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = NULL; - [[UIApplication sharedApplication] endReceivingRemoteControlEvents]; - MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; - commandCenter.playCommand.enabled = FALSE; - [commandCenter.playCommand removeTarget:playId]; - commandCenter.pauseCommand.enabled = FALSE; - [commandCenter.pauseCommand removeTarget:pauseId]; - commandCenter.previousTrackCommand.enabled = FALSE; - [commandCenter.nextTrackCommand removeTarget:nextTrackId]; - [commandCenter.previousTrackCommand removeTarget:previousTrackId]; - commandCenter.nextTrackCommand.enabled = FALSE; - [commandCenter.togglePlayPauseCommand removeTarget:togglePlayPauseId]; - - if (@available(iOS 15.0, *)) { - [commandCenter.seekForwardCommand removeTarget:seekForwardId]; - commandCenter.seekForwardCommand.enabled = FALSE; - [commandCenter.seekBackwardCommand removeTarget:seekBackwardId]; - commandCenter.seekBackwardCommand.enabled = FALSE; - [commandCenter.changePlaybackPositionCommand removeTarget:changePlaybackPositionId]; - commandCenter.changePlaybackPositionCommand.enabled = FALSE; - } - - NSLog(@"Player: MPRemote: Disabled Remote Command Center! Done!"); -} - --(void)disableNotificationCommands:(NSString *) playerId { - NSLog(@"Player: MPRemote: Disabling Remote Commands: START"); - NSMutableDictionary * playerInfo = players[playerId]; - [playerInfo setValue:@false forKey:@"areNotificationCommandsEnabled"]; - NSLog(@"Player: MPRemote: Disabled Remote Commands: END"); -} - --(void)enableNotificationCommands:(NSString *) playerId { - NSLog(@"Player: MPRemote: Enabling Remote Commands: START"); - NSMutableDictionary * playerInfo = players[playerId]; - [playerInfo setValue:@true forKey:@"areNotificationCommandsEnabled"]; - NSLog(@"Player: MPRemote: Enabling Remote Commands: END"); -} - - --(void)setCurrentResourceLoadingRequest: (AVAssetResourceLoadingRequest*) resourceLoadingRequest { - NSLog(@"===> set.resourceLoading: %@", resourceLoadingRequest); - if (currentResourceLoadingRequest != nil) { - if (!currentResourceLoadingRequest.cancelled && !currentResourceLoadingRequest.finished) { - [currentResourceLoadingRequest finishLoading]; - } - } - currentResourceLoadingRequest = resourceLoadingRequest; -} - - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString * playerId = @"call.arguments"; -// shallSendEvents = [call.arguments[@"shallSendEvents"] boolValue]; - - NSLog(@"Player: Method Call => call %@, playerId %@", call.method, playerId); - - typedef void (^CaseBlock)(void); - - - // Squint and this looks like a proper switch! - NSDictionary *methods = @{ - @"can_play": - ^{ - result(@(Ok)); - }, - @"remove_all":^{ - NSLog(@"Player: remove_all"); - result(@(Ok)); - }, - @"disable_repeat_mode":^{ - NSLog(@"Player: disable_repeat_mode"); - result(@(Ok)); - }, - @"enqueue": - ^{ - NSLog(@"Player: enqueue!"); - NSDictionary *batch = call.arguments; - NSArray *items = batch[@"batch"]; - BOOL autoPlay = [batch[@"autoPlay"] boolValue]; - NSString *cookie = batch[@"cookie"]; - - if (items == nil || [items count] == 0) { - result(@(NotOk)); - return; - } - - if (cookie != nil) { - // Atualizar o cookie se necessário - lastCookie = cookie; - } - //TODO: Passar para cá isLocal -// int isLocal = [call.arguments[@"isLocal"]intValue] ; - int isLocal = false; - float volume = (float)[call.arguments[@"volume"] doubleValue] ; - //TODO: Passar para cá respectSilence -// bool respectSilence = [call.arguments[@"respectSilence"]boolValue] ; - bool respectSilence = false ; - - playlistItems = [[NSMutableArray alloc] init]; - for (NSDictionary *item in items) { - NSString *albumId = [self ensureStringValue:item[@"albumId"]]; - NSString *albumTitle = [self ensureStringValue:item[@"albumTitle"]]; - NSString *name = [self ensureStringValue:item[@"name"]]; - NSString *author = [self ensureStringValue:item[@"author"]]; - NSString *url = [self ensureStringValue:item[@"url"]]; - NSString *coverUrl = [self ensureStringValue:item[@"coverUrl"]]; - int milliseconds = call.arguments[@"position"] == [NSNull null] ? 0.0 : [call.arguments[@"position"] intValue] ; - CMTime time = CMTimeMakeWithSeconds(milliseconds / 1000,NSEC_PER_SEC); - PlaylistItem *currentItem = [[PlaylistItem alloc] initWithAlbumId:albumId albumName:albumTitle title:name artist:author url:url coverUrl:coverUrl]; - [playlistItems addObject:currentItem]; - } - - NSLog(@"Player: Enqueuing %lu items", (unsigned long)[playlistItems count]); - for (PlaylistItem *item in playlistItems) { - NSLog(@"Player: Item - Title: %@, Artist: %@, Album: %@, URL: %@", - item.title, item.artist, item.albumName, item.url); - } - - - - - -// int ret = [self load:playerId mediaItems:playlistItems cookie:cookie isLocal:isLocal volume:volume isNotification:respectSilence]; - - - result(@(Ok)); - }, - @"play": - ^{ - NSLog(@"Player: play!"); - NSString *albumId = call.arguments[@"albumId"]; - NSString *albumTitle = call.arguments[@"albumTitle"]; - NSString *name = call.arguments[@"name"]; - NSString *author = call.arguments[@"author"]; - NSString *url = call.arguments[@"url"]; - NSString *coverUrl = call.arguments[@"coverUrl"]; - NSString *cookie = call.arguments[@"cookie"]; - if (albumId == nil) - result(0); - if (name == nil) - result(0); - if (author == nil) - result(0); - if (url == nil) - result(0); - if (cookie == nil) - result(0); - if (call.arguments[@"isLocal"] == nil) - result(0); - if (call.arguments[@"volume"] == nil) - result(0); - if (call.arguments[@"position"] == nil) - result(0); - if (call.arguments[@"respectSilence"] == nil) - result(0); - if (coverUrl == nil) { - coverUrl = DEFAULT_COVER; - } - int isLocal = [call.arguments[@"isLocal"]intValue] ; - float volume = (float)[call.arguments[@"volume"] doubleValue] ; - int milliseconds = call.arguments[@"position"] == [NSNull null] ? 0.0 : [call.arguments[@"position"] intValue] ; - bool respectSilence = [call.arguments[@"respectSilence"]boolValue] ; - CMTime time = CMTimeMakeWithSeconds(milliseconds / 1000,NSEC_PER_SEC); - - currentItem = [[PlaylistItem alloc] initWithAlbumId:albumId albumName:albumTitle title:name artist:author url:url coverUrl:coverUrl]; - - lastName = name; - lastAuthor = author; - lastUrl = url; - lastCoverUrl = coverUrl; - lastCookie = cookie; - lastVolume = volume; - lastTime = time; - lastRespectSilence = respectSilence; - - int ret = [self play:playerId name:name author:author url:url coverUrl:coverUrl cookie:cookie isLocal:isLocal volume:volume time:time isNotification:respectSilence]; - result(@(ret)); - }, - @"pause": - ^{ - NSLog(@"Player: pause"); - int ret = [self pause:playerId]; - result(@(ret)); - }, - @"resume": - ^{ - NSLog(@"Player: resume"); - int ret = [self resume:playerId]; - result(@(ret)); - }, - @"send_notification": - ^{ - NSLog(@"Player: send_notification"); - NSString *albumId = call.arguments[@"albumId"]; - NSString *albumTitle = call.arguments[@"albumTitle"]; - NSString *name = call.arguments[@"name"]; - NSString *author = call.arguments[@"author"]; - NSString *url = @"silence://from-asset"; - NSString *coverUrl = call.arguments[@"coverUrl"]; - NSString *cookie = call.arguments[@"cookie"]; - bool isPlaying = [call.arguments[@"isPlaying"]boolValue] ; - - if (albumId == nil) - result(0); - if (name == nil) - result(0); - if (author == nil) - result(0); - if (url == nil) - result(0); - if (cookie == nil) - result(0); - if (call.arguments[@"position"] == nil) - result(0); - if (call.arguments[@"duration"] == nil) - result(0); - if (coverUrl == nil) { - coverUrl = DEFAULT_COVER; - } - int position = call.arguments[@"position"] == [NSNull null] ? 0.0 : [call.arguments[@"position"] intValue]/1000 ; - int duration = call.arguments[@"duration"] == [NSNull null] ? 0.0 : [call.arguments[@"duration"] intValue]/1000 ; - CMTime time = CMTimeMakeWithSeconds(position,NSEC_PER_SEC); - - currentItem = [[PlaylistItem alloc] initWithAlbumId:albumId albumName:albumTitle title:name artist:author url:url coverUrl:coverUrl]; - - lastName = name; - lastAuthor = author; - lastUrl = url; - lastCoverUrl = coverUrl; - lastCookie = cookie; - lastTime = time; - - if (isPlaying == false) { - [self pause:playerId]; - } else if (position == 0) { - [self play:playerId name:name author:author url:url coverUrl:coverUrl cookie:cookie isLocal:true volume:0.0 time:time isNotification:true]; - } - [NowPlayingCenter setWithItem:currentItem]; - dispatch_async(dispatch_get_global_queue(0,0), ^{ - dispatch_async(dispatch_get_main_queue(), ^{ - [NowPlayingCenter updateWithItem:currentItem rate:1.0 position:position duration:duration]; - }); - }); - - - result(@(1)); - }, - @"remove_notification": - ^{ - NSLog(@"Player: remove_notification"); - [self disableRemoteCommandCenter:playerId]; - result(@(1)); - }, - @"disable_notification_commands": - ^{ - NSLog(@"Player: disable_notification_commands"); - [self disableNotificationCommands:playerId]; - result(@(1)); - }, - @"enable_notification_commands": - ^{ - NSLog(@"Player: enable_notification_commands"); - [self enableNotificationCommands:playerId]; - result(@(1)); - }, - @"stop": - ^{ - NSLog(@"Player: stop"); - [self stop:playerId]; - result(@(1)); - }, - @"release": - ^{ - NSLog(@"Player: release"); - [self stop:playerId]; - result(@(1)); - }, - @"seek": - ^{ - NSLog(@"Player: seek"); - if (!call.arguments[@"position"]) { - NSLog(@"Player: seek: position is null"); - result(0); - } else { - int milliseconds = [call.arguments[@"position"] intValue]; - NSLog(@"Player: Seeking to: %d milliseconds", milliseconds); - int ret = [self seek:playerId time:CMTimeMakeWithSeconds(milliseconds / 1000,NSEC_PER_SEC)]; - result(@(ret)); - } - }, - @"setUrl": - ^{ - NSLog(@"Player: setUrl"); - NSString *url = call.arguments[@"url"]; - NSString *cookie = call.arguments[@"cookie"]; - int isLocal = [call.arguments[@"isLocal"]intValue]; - int ret = [ self setUrl:url - isLocal:isLocal - cookie:cookie - playerId:playerId - shallPlay: true - onReady:^(NSString * playerId) { - result(@(1)); - } - ]; - result(@(ret)); - }, - @"getDuration": - ^{ - - int duration = [self getDuration:playerId]; - NSLog(@"Player: getDuration: %i ", duration); - result(@(duration)); - }, - @"getCurrentPosition": - ^{ - int currentPosition = [self getCurrentPosition:playerId]; - NSLog(@"Player: getCurrentPosition: %i ", currentPosition); - result(@(currentPosition)); - }, - @"setVolume": - ^{ - NSLog(@"Player: setVolume"); - float volume = (float)[call.arguments[@"volume"] doubleValue]; - [self setVolume:volume playerId:playerId]; - result(@(1)); - }, - @"setReleaseMode": - ^{ - NSLog(@"Player: setReleaseMode"); - NSString *releaseMode = call.arguments[@"releaseMode"]; - bool looping = [releaseMode hasSuffix:@"LOOP"]; - [self setLooping:looping playerId:playerId]; - result(@(1)); - } - }; - [ self initPlayerInfo:playerId ]; - CaseBlock c = methods[call.method]; - if (c) c(); else { - NSLog(@"Player: not implemented"); - result(FlutterMethodNotImplemented); - } -} - --(void) initPlayerInfo: (NSString *) playerId { - NSMutableDictionary * playerInfo = players[playerId]; - if (!playerInfo) { - players[playerId] = [@{@"isPlaying": @false, - @"pausedByVoiceSearch": @false, - @"pausedByInterruption": @false, - @"volume": @(1.0), - @"looping": @(false), - @"areNotificationCommandsEnabled": @(true), - @"isSeeking": @(false), - @"failedToStartPlaying": @(false), - } mutableCopy]; - _playerId = playerId; - } -} - --(void) setCurrentItem: (NSString *) playerId - name:(NSString *) name - author:(NSString *) author - url:(NSString *) url - coverUrl:(NSString *) coverUrl -{ - NSLog(@"Player: playerId=%@ name=%@ author=%@ url=%@ coverUrl=%@", playerId, name, author, url, coverUrl); - playersCurrentItem[playerId] = @{ - @"name": name, - @"author": author, - @"url": url, - @"coverUrl": coverUrl}; -} - --(NSString*) replaceScheme: (NSString*) oldScheme - newScheme: (NSString*) newScheme - fromUrl: (NSString*) url { - NSURLComponents *components = [NSURLComponents componentsWithString: url]; - if ([components.scheme rangeOfString: oldScheme].location != NSNotFound) { - components.scheme = newScheme; - return components.URL.absoluteString; - } - - return url; -} - - --(void) configurePlayer:(NSString *)playerId url:(NSString *)url { - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - if (@available(iOS 10.0, *)) { - if ([url rangeOfString: m3u8Ext].location != NSNotFound) { - player.automaticallyWaitsToMinimizeStalling = TRUE; - } else{ - player.automaticallyWaitsToMinimizeStalling = FALSE; - } - } -} - --(NSString *)ensureStringValue:(id)value { - if ([value isKindOfClass:[NSString class]]) { - return value; - } else if ([value isKindOfClass:[NSNumber class]]) { - return [(NSNumber *)value stringValue]; - } - return @""; -} - --(void) initAVPlayer:(NSString *)playerId playerItem:(AVPlayerItem *)playerItem url:(NSString *)url onReady:(VoidCallback) onReady { - NSMutableDictionary * playerInfo = players[_playerId]; - __block AVQueuePlayer *player = nil; - - dispatch_async (playerQueue, ^{ - player = [[ AVQueuePlayer alloc ] init]; - [self configurePlayer: playerId url:url]; - player.allowsExternalPlayback = FALSE; - [player replaceCurrentItemWithPlayerItem:playerItem]; - NSMutableSet *observers = [[NSMutableSet alloc] init]; - - [ playerInfo setObject:player forKey:@"player" ]; - [ playerInfo setObject:url forKey:@"url" ]; - [ playerInfo setObject:observers forKey:@"observers"]; - - CMTime interval = CMTimeMakeWithSeconds(0.9, NSEC_PER_SEC); - id timeObserver = [player addPeriodicTimeObserverForInterval: interval queue: nil usingBlock:^(CMTime time){ - [self onTimeInterval:playerId time:time]; - }]; - [timeobservers addObject:@{@"player":player, @"observer":timeObserver}]; - - id avrouteobserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVAudioSessionRouteChangeNotification - object: nil - queue: NSOperationQueue.mainQueue - usingBlock:^(NSNotification* notification){ - NSDictionary *dict = notification.userInfo; - NSLog(@"Player: AVAudioSessionRouteChangeNotification received. UserInfo: %@", dict); - NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; - NSMutableDictionary *playerInfo = players[_playerId]; - - switch (reason.unsignedIntegerValue) { - case AVAudioSessionRouteChangeReasonCategoryChange: - { - NSString *category = [[AVAudioSession sharedInstance] category]; - if ([category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) { - if(wasPlayingBeforeVoiceSearch == -1){ - wasPlayingBeforeVoiceSearch = [playerInfo[@"isPlaying"] intValue]; - } - NSLog(@"Category: %@, wasPlaying: %i PAUSE", category, wasPlayingBeforeVoiceSearch); - [playerInfo setValue:@(true) forKey:@"pausedByVoiceSearch"]; - [self pause:_playerId]; - } - if ([category isEqualToString:AVAudioSessionCategoryPlayback]) { - NSLog(@"Category: %@ RESUME %@", category, playerInfo[@"pausedByVoiceSearch"]); - int pausedByVoice = [playerInfo[@"pausedByVoiceSearch"] intValue]; - if (pausedByVoice == 1 && wasPlayingBeforeVoiceSearch == 1) { - [playerInfo setValue:@(false) forKey:@"pausedByVoiceSearch"]; - [self resume:_playerId]; - } else { - NSLog(@"No operation required!"); - } - wasPlayingBeforeVoiceSearch = -1; - } - } - break; - - case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: - [self pause:_playerId]; - break; - default: - break; - } - }]; - id avlostobserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVAudioSessionMediaServicesWereLostNotification - object: nil - queue: NSOperationQueue.mainQueue - usingBlock:^(NSNotification* note){ - NSDictionary *dict = note.userInfo; - NSLog(@"Player: AVAudioSessionMediaServicesWereLostNotification received. UserInfo: %@", dict); - }]; - id avrestartobserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVAudioSessionMediaServicesWereResetNotification - object: nil - queue: NSOperationQueue.mainQueue - usingBlock:^(NSNotification* note){ - NSDictionary *dict = note.userInfo; - NSLog(@"Player: AVAudioSessionMediaServicesWereResetNotification received. UserInfo: %@", dict); - NSLog(@"Player: Player Error: %lu", (unsigned long)[player hash]); - [self disposePlayer]; - [self setUrl:latestUrl isLocal:latestIsLocal cookie:latestCookie playerId:latestPlayerId shallPlay: true onReady:latestOnReady]; - }]; - - AVAudioSession *aSession = [AVAudioSession sharedInstance]; - id interruptionObserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVAudioSessionInterruptionNotification - object: aSession - queue: NSOperationQueue.mainQueue - usingBlock:^(NSNotification* notification){ - NSDictionary *dict = notification.userInfo; - NSLog(@"Player: AVAudioSessionInterruptionNotification received. UserInfo: %@", dict); - NSNumber *interruptionType = [[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey]; - NSNumber *interruptionOption = [[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey]; - - NSMutableDictionary *playerInfo = players[_playerId]; - switch (interruptionType.unsignedIntegerValue) { - case AVAudioSessionInterruptionTypeBegan:{ - int isPlaying = [playerInfo[@"isPlaying"] intValue]; - if (isPlaying == 1) { - [playerInfo setValue:@(true) forKey:@"pausedByInterruption"]; - [self pause:_playerId]; - } - } break; - case AVAudioSessionInterruptionTypeEnded:{ - if (interruptionOption.unsignedIntegerValue == AVAudioSessionInterruptionOptionShouldResume) { - int pausedByInterruption = [playerInfo[@"pausedByInterruption"] intValue]; - if (pausedByInterruption == 1) { - [playerInfo setValue:@(false) forKey:@"pausedByInterruption"]; - [self resume:_playerId]; - } - } - } break; - default: - break; - } - - }]; - - [observers addObject:avlostobserver]; - [observers addObject:avrouteobserver]; - [observers addObject:avrestartobserver]; - [observers addObject:interruptionObserver]; - - // is sound ready - [playerInfo setObject:onReady forKey:@"onReady"]; - }); - - -} - --(void)observePlayerItem:(AVPlayerItem *)playerItem playerId:(NSString *)playerId { - if (latestPlayerItemObserved != playerItem) { - if (latestPlayerItemObserved != nil) { - NSLog(@"Player: latestPlayerItemObserved"); - - [self disposePlayerItem:latestPlayerItemObserved]; - } - - - alreadyhasEnded = false; - shouldAutoStart = true; - [playerItem addObserver:self - forKeyPath:@"status" - options:NSKeyValueObservingOptionNew - context:nil]; - - [playerItem addObserver:self - forKeyPath:@"playbackBufferEmpty" - options:NSKeyValueObservingOptionNew - context:nil]; - - [playerItem addObserver:self - forKeyPath:@"playbackLikelyToKeepUp" - options:NSKeyValueObservingOptionNew - context:nil]; - - [playerItem addObserver:self - forKeyPath:@"playbackBufferFull" - options:NSKeyValueObservingOptionNew - context:nil]; - - NSMutableSet *observers = [[NSMutableSet alloc] init]; - - id timeEndObserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVPlayerItemDidPlayToEndTimeNotification - object: playerItem - queue: nil - usingBlock:^(NSNotification* note){ - [self onSoundComplete:playerId]; - }]; - - - id jumpedItemObserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVPlayerItemTimeJumpedNotification - object: playerItem - queue: nil - usingBlock:^(NSNotification* note){ - NSMutableDictionary * playerInfo = players[_playerId]; - [playerInfo setValue:@(false) forKey:@"isSeeking"]; - int state = STATE_SEEK_END; - [self notifyStateChange:_playerId state:state overrideBlock:false]; - NSLog(@"Player: AVPlayerItemTimeJumpedNotification: %@", [note object]); - }]; - id failedEndTimeObserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVPlayerItemFailedToPlayToEndTimeNotification - object: playerItem - queue: nil - usingBlock:^(NSNotification* note){ - // item has failed to play to its end time - if (isConnected || latestIsLocal) { - NSLog(@"Player: AVPlayerItemFailedToPlayToEndTimeNotification: %@", [note object]); - [self notifyOnError:_playerId errorType:PLAYER_ERROR_FAILED]; - } else { - stopTryingToReconnect = true; - notifiedBufferEmptyWithNoConnection = true; -#ifdef ENABLE_PLAYER_NETWORK_ERROR - if (!notifiedBufferEmptyWithNoConnection) { - [self notifyOnError:_playerId errorType:PLAYER_ERROR_NETWORK_ERROR]; - } -#endif - } - }]; - id stalledObserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVPlayerItemPlaybackStalledNotification - object: playerItem - queue: nil - usingBlock:^(NSNotification* note){ - // media did not arrive in time to continue playback - NSLog(@"Player: AVPlayerItemPlaybackStalledNotification: %@", [note object]); - }]; - id newAccessLogObserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVPlayerItemNewAccessLogEntryNotification - object: playerItem - queue: nil - usingBlock:^(NSNotification* note){ - // a new access log entry has been added - NSLog(@"Player: AVPlayerItemNewAccessLogEntryNotification: %@", [note object]); - }]; - id newAccessLogErrorObserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVPlayerItemNewErrorLogEntryNotification - object: playerItem - queue: nil - usingBlock:^(NSNotification* note){ - // a new access log entry error has been added - NSLog(@"Player: AVPlayerItemNewErrorLogEntryNotification: %@", [note object]); - AVPlayerItemErrorLog *errorLog = [playerItem errorLog]; - NSLog(@"Player: AVPlayerItemNewErrorLogEntryNotification: %@", errorLog); - -#ifdef ENABLE_PLAYER_NETWORK_ERROR - // we decided to remove this - if (!notifiedBufferEmptyWithNoConnection) { - [self notifyOnError:_playerId errorType:PLAYER_ERROR_NETWORK_ERROR]; - notifiedBufferEmptyWithNoConnection = true; - } - [self pause:_playerId]; -#endif - - }]; - if (@available(iOS 13.0, *)) { - id selectionDidChangeObserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVPlayerItemMediaSelectionDidChangeNotification - object: playerItem - queue: nil - usingBlock:^(NSNotification* note){ - NSLog(@"Player: AVPlayerItemMediaSelectionDidChangeNotification: %@", [note object]); - }]; - id timeOffsetFromLiveObserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVPlayerItemRecommendedTimeOffsetFromLiveDidChangeNotification - object: playerItem - queue: nil - usingBlock:^(NSNotification* note){ - // the value of recommendedTimeOffsetFromLive has changed - NSLog(@"Player: AVPlayerItemRecommendedTimeOffsetFromLiveDidChangeNotification: %@", [note object]); - }]; - - [observers addObject:selectionDidChangeObserver]; - [observers addObject:timeOffsetFromLiveObserver]; - } - id failedToPlayEndTimeObserver = [[ NSNotificationCenter defaultCenter ] addObserverForName: AVPlayerItemFailedToPlayToEndTimeErrorKey - object: playerItem - queue: nil - usingBlock:^(NSNotification* note){ - // NSError - NSLog(@"Player: AVPlayerItemFailedToPlayToEndTimeErrorKey: %@", [note object]); - [self notifyOnError:_playerId errorType:PLAYER_ERROR_FAILED]; - }]; - - - NSMutableDictionary * playerInfo = players[_playerId]; - - [observers addObject:timeEndObserver]; - [observers addObject:jumpedItemObserver]; - [observers addObject:failedEndTimeObserver]; - [observers addObject:stalledObserver]; - [observers addObject:newAccessLogObserver]; - [observers addObject:newAccessLogErrorObserver]; - [observers addObject:failedToPlayEndTimeObserver]; - - [playerInfo setObject:observers forKey:@"observers_player_item"]; - latestPlayerItemObserved = playerItem; - } -} - --(void)disposePlayerItem:(AVPlayerItem *)playerItem { - if (playerItem == nil) { - return; - } - if (latestPlayerItemObserved == playerItem) { - NSLog(@"Player: disposing Player Items : START"); - - @try { - [playerItem removeObserver:self forKeyPath:@"status" context:nil]; - } @catch (NSException * __unused exception) { - NSLog(@"Player: failed dispose status %@", exception); - } - @try { - [playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp" context:nil]; - } @catch (NSException * __unused exception) { - NSLog(@"Player: failed dispose playbackLikelyToKeepUp %@", exception); - } - @try { - [playerItem removeObserver:self forKeyPath:@"playbackBufferFull" context:nil]; - } @catch (NSException * __unused exception) { - NSLog(@"Player: failed dispose playbackBufferFull %@", exception); - } - @try { - [playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty" context:nil]; - } @catch (NSException * __unused exception) { - NSLog(@"Player: failed dispose playbackBufferEmpty %@", exception); - } - - NSMutableDictionary * playerInfo = players[_playerId]; - NSMutableSet *observers = playerInfo[@"observers_player_item"]; - - for (id ob in observers) { - @try { - [ [ NSNotificationCenter defaultCenter ] removeObserver:ob ]; - } @catch (NSException * __unused exception) { - NSLog(@"Player: failed remove removeObserver %@ : %@", ob, exception); - } - } - latestPlayerItemObserved = nil; - NSLog(@"Player: disposing Player Items : END"); - } -} - -- (void)treatPlayerObservers:(AVPlayer *)player url:(NSString *)url { - NSMutableDictionary * playerInfo = players[_playerId]; - NSMutableSet *observers = playerInfo[@"observers"]; - NSLog(@"Player: entered treatPlayerObservers "); - - @try { - [[player currentItem] removeObserver:self forKeyPath:@"status" ]; - } @catch (NSException * __unused exception) { - NSLog(@"Player: failed dispose status %@", exception); - } - - for (id ob in observers) { - @try { - [ [ NSNotificationCenter defaultCenter ] removeObserver:ob ]; - } @catch (NSException * __unused exception) { NSLog(@"Player: failed dispose generic %@", exception); - } - } - [ observers removeAllObjects ]; -} - --(int) setUrl: (NSString*) url - isLocal: (bool) isLocal - cookie: (NSString*) cookie - playerId: (NSString*) playerId - shallPlay: (bool) shallPlay - onReady:(VoidCallback)onReady -{ - if([url containsString:@"silence://from-asset"]){ - url = MINUTES_OF_SILENCE; - } - NSLog(@"Player: setUrl url: %@ cookie: %@", url, cookie); - currentResourceLoader = nil; - [self disposePlayerItem:latestPlayerItemObserved]; - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - [self configurePlayer: playerId url:url]; - - __block AVPlayerItem *playerItem; - @try { - if (!playerInfo || ![url isEqualToString:playerInfo[@"url"]] || [url containsString:@"silence.mp3"] ) { - NSLog(@"Player: Loading new URL"); - if (isLocal) { - NSLog(@"Player: Item is Local"); - playerItem = [ [ AVPlayerItem alloc ] initWithURL:[ NSURL fileURLWithPath:url ]]; - if (shallPlay) { - [self playItem:playerItem url:url onReady:onReady]; - } else { - [self loadItem:playerItem url:url onReady:onReady]; - } - } else { - NSLog(@"Player: Item is Remote"); - NSURLComponents *components = [NSURLComponents componentsWithURL:[NSURL URLWithString:url] resolvingAgainstBaseURL:YES]; - if ([components.path rangeOfString: m3u8Ext].location != NSNotFound) { - NSLog(@"Player: Item is m3u8"); - components.scheme = customPlaylistScheme; - url = components.URL.absoluteString; - NSLog(@"Player: newUrl: %@", url); - } - - NSURL *_url = [NSURL URLWithString: url]; - NSURL *_urlWildcard = [NSURL URLWithString: @"*.suamusica.com.br/*"]; - NSHTTPCookieStorage *cookiesStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - [cookiesStorage removeCookiesSinceDate:[NSDate dateWithTimeIntervalSince1970:0]]; - - NSArray *cookiesItems = [cookie componentsSeparatedByString:@";"]; - for (NSString *cookieItem in cookiesItems) { - NSArray *keyValue = [cookieItem componentsSeparatedByString:@"="]; - if ([keyValue count] == 2) { - NSString *key = [keyValue objectAtIndex:0]; - NSString *value = [keyValue objectAtIndex:1]; - NSHTTPCookie *httpCookie = [ [NSHTTPCookie cookiesWithResponseHeaderFields:@{@"Set-Cookie": [NSString stringWithFormat:@"%@=%@", key, value]} forURL:_urlWildcard] objectAtIndex:0]; - - @try { - [cookiesStorage setCookie:httpCookie]; - } - @catch (NSException *exception) { - NSLog(@"Player: %@", exception.reason); - } - } - } - - NSMutableDictionary * headers = [NSMutableDictionary dictionary]; - [headers setObject:@"mp.next" forKey:@"User-Agent"]; - [headers setObject:cookie forKey:@"Cookie"]; - - AVURLAsset * asset = [AVURLAsset URLAssetWithURL:_url options:@{@"AVURLAssetHTTPHeaderFieldsKey": headers, AVURLAssetHTTPCookiesKey : [cookiesStorage cookies] }]; - currentResourceLoader = [asset resourceLoader]; - [[asset resourceLoader] setDelegate:(id)self queue:serialQueue]; - - NSArray *keys = @[@"playable"]; - [asset loadValuesAsynchronouslyForKeys:keys completionHandler:^() { - NSError *error = nil; - AVKeyValueStatus status = [asset statusOfValueForKey:@"playable" error:&error]; - switch (status) { - case AVKeyValueStatusLoaded: - playerItem = [AVPlayerItem playerItemWithAsset:asset]; - break; - - case AVKeyValueStatusUnknown: - case AVKeyValueStatusFailed: - case AVKeyValueStatusCancelled: - NSLog(@"loadValuesAsynchronouslyForKeys: ERROR: %@", error); - playerItem = nil; - break; - - default: - break; - } - - if (shallPlay) { - [self playItem:playerItem url:url onReady:onReady]; - } else { - [self loadItem:playerItem url:url onReady:onReady]; - } - }]; - } - } else { - NSLog(@"Player: player or item is nil player: [%@] item: [%@]", player, [player currentItem]); - if (player == nil && [player currentItem] == nil) { - NSLog(@"Player: player status: %ld",(long)[[player currentItem] status ]); - - [self initAVPlayer:playerId playerItem:playerItem url:url onReady: onReady]; - [self observePlayerItem:playerItem playerId:playerId]; - } else if ([[player currentItem] status ] == AVPlayerItemStatusReadyToPlay) { - NSLog(@"Player: item ready to play"); - [self observePlayerItem:[player currentItem] playerId:playerId]; - [ playerInfo setObject:@true forKey:@"isPlaying" ]; - int state = STATE_PLAYING; - [self notifyStateChange:playerId state:state overrideBlock:false]; - onReady(playerId); - } else if ([[player currentItem] status ] == AVPlayerItemStatusFailed) { - NSLog(@"Player: FAILED STATUS. Notifying app that an error happened."); - [self disposePlayerItem:[player currentItem]]; - [self notifyOnError:playerId errorType:PLAYER_ERROR_FAILED]; - } else { - NSLog(@"Player: player status: %ld",(long)[[player currentItem] status ]); - NSLog(@"Player: If status 0 wait player reload alone."); - } - } - - return Ok; - } - - @catch (NSException *exception) { - NSLog(@"Player: Exception on setUrl: %@", exception); - } - @finally { - NSLog(@"Player: Finally condition"); - } -} - --(void) loadItem:(AVPlayerItem *)playerItem - url:(NSString *) url - onReady:(VoidCallback)onReady { - NSMutableDictionary * playerInfo = players[_playerId]; - AVPlayer *player = playerInfo[@"player"]; - - if (playerItem == nil) { - [_channel_player invokeMethod:@"audio.onError" arguments:@{@"playerId": _playerId, @"errorType": @(PLAYER_ERROR_FAILED)}]; - } - - if (playerInfo[@"url"]) { - NSLog(@"Player: Replacing item"); - @autoreleasepool { - [self observePlayerItem:playerItem playerId:_playerId]; - dispatch_async (playerQueue, ^{ - @try { - [ player replaceCurrentItemWithPlayerItem: playerItem ]; - NSLog(@"Player: Pausing"); - [self pause:_playerId]; - [ playerInfo setObject:@false forKey:@"isPlaying" ]; - [ playerInfo setObject:url forKey:@"url" ]; - notifiedBufferEmptyWithNoConnection = false; - stopTryingToReconnect = false; - [NowPlayingCenter setWithItem:currentItem]; - } @catch (NSException * __unused exception) { - NSLog(@"Player: failed to replaceCurrentItemWithPlayerItem %@", exception); - } - }); - } - } else { - NSLog(@"Player: Initing AVPPlayer"); - [self observePlayerItem:playerItem playerId:_playerId]; - [self initAVPlayer:_playerId playerItem:playerItem url:url onReady: onReady]; - } -} - --(void) playItem:(AVPlayerItem *)playerItem - url:(NSString *) url - onReady:(VoidCallback)onReady { - NSMutableDictionary * playerInfo = players[_playerId]; - AVPlayer *player = playerInfo[@"player"]; - - if (playerItem == nil) { - [self notifyOnError:_playerId errorType:PLAYER_ERROR_FAILED]; - } - - if (playerInfo[@"url"]) { - NSLog(@"Player: Replacing item"); - @autoreleasepool { - [self observePlayerItem:playerItem playerId:_playerId]; - dispatch_async (playerQueue, ^{ - @try { - [ player replaceCurrentItemWithPlayerItem: playerItem ]; - } @catch (NSException * __unused exception) { - NSLog(@"Player: failed to replaceCurrentItemWithPlayerItem %@", exception); - } - }); - } - } else { - NSLog(@"Player: Initing AVPPlayer"); - [self observePlayerItem:playerItem playerId:_playerId]; - [self initAVPlayer:_playerId playerItem:playerItem url:url onReady: onReady]; - } - NSLog(@"Player: Resuming"); - [self resume:_playerId]; - int state = STATE_BUFFERING; - [self notifyStateChange:_playerId state:state overrideBlock:false]; - [ playerInfo setObject:@false forKey:@"isPlaying" ]; - [ playerInfo setObject:url forKey:@"url" ]; - - notifiedBufferEmptyWithNoConnection = false; - stopTryingToReconnect = false; - - [NowPlayingCenter setWithItem:currentItem]; -} - -- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest{ - // NSLog(@"loadingRequest.URL: %@", [[loadingRequest request] URL]); - NSString* scheme = [[[loadingRequest request] URL] scheme]; - if (currentResourceLoader != resourceLoader) { - return NO; - } - - if ([self isRedirectSchemeValid:scheme]) { - return [self handleRedirectRequest:loadingRequest]; - } - - if ([self isCustomPlaylistSchemeValid:scheme]) { - dispatch_async (serialQueue, ^ { - [self handleCustomPlaylistRequest:loadingRequest]; - }); - return YES; - } - - return NO; -} - -- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { - if (currentResourceLoadingRequest != nil && currentResourceLoadingRequest.request == loadingRequest.request) { - [self setCurrentResourceLoadingRequest:nil]; - } -} - -- (BOOL) isCustomPlaylistSchemeValid:(NSString *)scheme -{ - return ([customPlaylistScheme isEqualToString:scheme]); -} - -/*! - * Handles the custom play list scheme: - * - * 1) Verifies its a custom playlist request, otherwise report an error. - * 2) Generates the play list. - * 3) Create a reponse with the new URL and report success. - */ -- (BOOL) handleCustomPlaylistRequest:(AVAssetResourceLoadingRequest *)loadingRequest -{ - [self setCurrentResourceLoadingRequest:loadingRequest]; - NSString* url = [[[loadingRequest request] URL] absoluteString]; - __block NSString *requestUrl = [self replaceScheme:customPlaylistScheme newScheme:httpsScheme fromUrl:url]; - __block NSString *playlistRequest = [self replaceScheme:customPlaylistScheme newScheme:redirectScheme fromUrl:url]; - - NSHTTPCookieStorage *cookiesStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:[cookiesStorage cookies]]; - - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; - [request setHTTPMethod:@"GET"]; - [request setURL:[NSURL URLWithString:requestUrl]]; - [request setAllHTTPHeaderFields:headers]; - - dispatch_semaphore_t sema = dispatch_semaphore_create(0); - - NSLog(@"Player: ==> requestURL: %@", [[request URL] absoluteString]); - NSURLSession *session = [NSURLSession sharedSession]; - [[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable _data, NSURLResponse * _Nullable _response, NSError * _Nullable error) { - NSHTTPURLResponse *responseCode = (NSHTTPURLResponse *) _response; - - if([responseCode statusCode] != 200) { - NSLog(@"Player: Error getting %@, HTTP status code %li", requestUrl, (long)[responseCode statusCode]); - [self reportError:loadingRequest withErrorCode:badRequestErrorCode]; - dispatch_semaphore_signal(sema); - } - - NSString* file = [[NSString alloc] initWithData:_data encoding:NSUTF8StringEncoding]; - NSMutableArray* lines = [file componentsSeparatedByString:@"\n"].mutableCopy; - - NSMutableArray* splittedUrl = [playlistRequest componentsSeparatedByString:@"/"].mutableCopy; - if ([[splittedUrl lastObject] rangeOfString:m3u8Ext].location != NSNotFound) { - [splittedUrl removeLastObject]; - } - __block NSString* baseUrl = [splittedUrl componentsJoinedByString:@"/"]; - NSLog(@"Player: ==> baseURL: %@", baseUrl); - - for (int i = 0; i < [lines count]; i++) { - NSString* line = lines[i]; - if ([line rangeOfString:extInfo].location != NSNotFound) { - i++; - NSString* treatedUrl = [lines[i] stringByReplacingOccurrencesOfString:@" " withString:@"+"]; - lines[i] = [NSString stringWithFormat:@"%@/%@", baseUrl, treatedUrl]; - } - } - NSString* _file = [lines componentsJoinedByString:@"\n"]; - NSLog(@"Player: %@", _file); - - NSData* data = [_file dataUsingEncoding:NSUTF8StringEncoding]; - if (data) - { - [loadingRequest.dataRequest respondWithData:data]; - [loadingRequest finishLoading]; - } else - { - [self reportError:loadingRequest withErrorCode:badRequestErrorCode]; - } - dispatch_semaphore_signal(sema); - }] resume]; - - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); - - return YES; -} - -/*! - * Validates the given redirect schme. - */ -- (BOOL) isRedirectSchemeValid:(NSString *)scheme -{ - return ([redirectScheme isEqualToString:scheme]); -} - --(NSURLRequest* ) generateRedirectURL:(NSURLRequest *)sourceURL -{ - NSHTTPCookieStorage *cookiesStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - NSDictionary * headers = [NSHTTPCookie requestHeaderFieldsWithCookies:[cookiesStorage cookies]]; - NSMutableURLRequest *redirect = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[[[sourceURL URL] absoluteString] stringByReplacingOccurrencesOfString:redirectScheme withString:httpsScheme]]]; - [redirect setAllHTTPHeaderFields:headers]; - NSLog(@"Player: ==> Redirect.URL: %@", [[redirect URL] absoluteString]); - return redirect; -} -/*! - * The delegate handler, handles the received request: - * - * 1) Verifies its a redirect request, otherwise report an error. - * 2) Generates the new URL - * 3) Create a reponse with the new URL and report success. - */ -- (BOOL) handleRedirectRequest:(AVAssetResourceLoadingRequest *)loadingRequest -{ - NSURLRequest *redirect = nil; - [self setCurrentResourceLoadingRequest:loadingRequest]; - - redirect = [self generateRedirectURL:(NSURLRequest *)[loadingRequest request]]; - if (redirect) - { - [loadingRequest setRedirect:redirect]; - NSHTTPCookieStorage *cookiesStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - NSDictionary * headers = [NSHTTPCookie requestHeaderFieldsWithCookies:[cookiesStorage cookies]]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[redirect URL] statusCode:redirectErrorCode HTTPVersion:nil headerFields:headers]; - [loadingRequest setResponse:response]; - [loadingRequest finishLoading]; - } else - { - [self reportError:loadingRequest withErrorCode:badRequestErrorCode]; - } - return YES; -} - -- (void) reportError:(AVAssetResourceLoadingRequest *) loadingRequest withErrorCode:(int) error -{ - NSMutableDictionary * playerInfo = players[_playerId]; - [playerInfo setValue:@(true) forKey:@"failedToStartPlaying"]; - NSLog(@"Player: reportError.error: %d",error); - [loadingRequest finishLoadingWithError:[NSError errorWithDomain: NSURLErrorDomain code:error userInfo: nil]]; -} - --(int) ensureConnected: (NSString*) playerId - isLocal: (int) isLocal -{ -#ifdef ENABLE_PLAYER_NETWORK_ERROR - // we decided to remove this - if (!isConnected && !isLocal) { - [self notifyOnError:_playerId errorType:PLAYER_ERROR_NETWORK_ERROR]; - return -1; - } -#endif - return Ok; -} - --(int) configureAudioSession: (NSString *) playerId -{ - return [AudioSessionManager activeSession] ? Ok : NotOk; -} - --(int) load: (NSString*) playerId -playlistItems:(NSMutableArray*) playlistItems - cookie: (NSString *) cookie - isLocal: (int) isLocal - volume: (float) volume -isNotification: (bool) respectSilence -{ -// loadOnly = true; -// if ([self ensureConnected:playerId isLocal:isLocal] == -1) { -// return -1; -// } -// -// NSMutableDictionary * playerInfo = players[playerId]; -// AVPlayer *player = playerInfo[@"player"]; -// if (player.rate != 0) { -// [player pause]; -// } -// -// -// -// for (PlaylistItem *item in playlistItems) { -// if (!@available(iOS 11,*)) { -// item.url = [item.url stringByReplacingOccurrencesOfString:@".m3u8" -// withString:@".mp3"]; -// item.url = [item.url stringByReplacingOccurrencesOfString:@"stream/" -// withString:@""]; -// } -// -// -// latestUrl = item.url; -// latestIsLocal = isLocal; -// latestCookie = cookie; -// latestPlayerId = playerId; -// latestOnReady = ^(NSString * playerId) { -// NSLog(@"Player: Inside OnReady"); -// NSMutableDictionary * playerInfo = players[playerId]; -// AVPlayer *player = playerInfo[@"player"]; -// [ player setVolume:volume ]; -// [ player seekToTime:item.duration ]; -// if(!loadOnly){ -// [ player play]; -// } -// }; -// -// NSLog(@"Player: Volume: %f", volume); -// -// [self configureRemoteCommandCenter]; -// if ([self configureAudioSession:playerId] != Ok) { -// if (!loadOnly) { -// [_channel_player invokeMethod:@"audio.onError" arguments:@{@"playerId": _playerId, @"errorType": @(PLAYER_ERROR_FAILED)}]; -// return NotOk; -// } -// } -// -// if (item.name == nil) { -// item.name = @"unknown"; -// } -// -// if (item.author == nil) { -// item.author = @"unknown"; -// } -// -// if (item.coverUrl == nil) { -// item.coverUrl = DEFAULT_COVER; -// } -// -// NSLog(@"Player: [SET_CURRENT_ITEM LOG] playerId=%@ name=%@ author=%@ url=%@ coverUrl=%@", playerId, name, author, url, coverUrl); -// [self setCurrentItem:playerId name:name author:author url:url coverUrl:coverUrl]; -// -// -// [self setUrl:url -// isLocal:isLocal -// cookie:cookie -// playerId:playerId -// shallPlay: false -// onReady:latestOnReady]; -// } - - - - return Ok; -} - --(int) play: (NSString*) playerId - name: (NSString*) name - author: (NSString*) author - url: (NSString*) url - coverUrl: (NSString*) coverUrl - cookie: (NSString *) cookie - isLocal: (int) isLocal - volume: (float) volume - time: (CMTime) time -isNotification: (bool) respectSilence -{ - loadOnly = false; - if ([self ensureConnected:playerId isLocal:isLocal] == -1) { - return -1; - } - - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - if (player.rate != 0) { - [player pause]; - } - - if (!@available(iOS 11,*)) { - url = [url stringByReplacingOccurrencesOfString:@".m3u8" - withString:@".mp3"]; - url = [url stringByReplacingOccurrencesOfString:@"stream/" - withString:@""]; - } - latestUrl = url; - latestIsLocal = isLocal; - latestCookie = cookie; - latestPlayerId = playerId; - latestOnReady = ^(NSString * playerId) { - NSLog(@"Player: Inside OnReady"); - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - [ player setVolume:volume ]; - [ player seekToTime:time ]; - [ player play]; - }; - - NSLog(@"Player: Volume: %f", volume); - - [self configureRemoteCommandCenter]; - if ([self configureAudioSession:playerId] != Ok) { - [self notifyOnError:playerId errorType:PLAYER_ERROR_FAILED]; - return NotOk; - } - - if (name == nil) { - name = @"unknown"; - } - - if (author == nil) { - author = @"unknown"; - } - - if (coverUrl == nil) { - coverUrl = DEFAULT_COVER; - } - - NSLog(@"Player: [SET_CURRENT_ITEM LOG] playerId=%@ name=%@ author=%@ url=%@ coverUrl=%@", playerId, name, author, url, coverUrl); - [self setCurrentItem:playerId name:name author:author url:url coverUrl:coverUrl]; - - - [self setUrl:url - isLocal:isLocal - cookie:cookie - playerId:playerId - shallPlay: true - onReady:latestOnReady]; - - return Ok; - -} - --(void) updateDuration: (NSString *) playerId -{ - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - - CMTime duration = [[[player currentItem] asset] duration]; - NSLog(@"Player: ios -> updateDuration...%f", CMTimeGetSeconds(duration)); - if(CMTimeGetSeconds(duration)>0){ - int durationInMilliseconds = CMTimeGetSeconds(duration)*1000; - if (shallSendEvents) { - [_channel_player invokeMethod:@"audio.onDuration" arguments:@{@"playerId": playerId, @"duration": @(durationInMilliseconds)}]; - } - } -} - --(int) getDuration: (NSString *) playerId { - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - - CMTime duration = [[[player currentItem] asset] duration]; - int mseconds= CMTimeGetSeconds(duration)*1000; - return mseconds; -} - --(int) getCurrentPosition: (NSString *) playerId { - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - - CMTime duration = [player currentTime]; - int durationInMilliseconds = CMTimeGetSeconds(duration)*1000; - return durationInMilliseconds; -} - --(void) onTimeInterval: (NSString *) playerId - time: (CMTime) time { - NSMutableDictionary * playerInfo = players[_playerId]; - if (![playerInfo[@"isSeeking"] boolValue]) { - int position = CMTimeGetSeconds(time); - AVPlayer *player = playerInfo[@"player"]; - - // let us save it - lastTime = time; - - CMTime duration = [[[player currentItem] asset] duration]; - int _duration = CMTimeGetSeconds(duration); - - NSDictionary *currentItemProps = playersCurrentItem[playerId]; - NSString *name = currentItemProps[@"name"]; - NSString *author = currentItemProps[@"author"]; - NSString *coverUrl = currentItemProps[@"coverUrl"]; - if (name == nil || author == nil || coverUrl == nil){ - name = @"Sua Musica"; - author = @"Sua Musica"; - coverUrl = DEFAULT_COVER; - } - - int durationInMillis = _duration*1000; - int positionInMillis = position*1000; - - if (shallSendEvents) { - [_channel_player invokeMethod:@"audio.onCurrentPosition" arguments:@{@"playerId": playerId, @"position": @(positionInMillis), @"duration": @(durationInMillis)}]; - - dispatch_async(dispatch_get_global_queue(0,0), ^{ - dispatch_async(dispatch_get_main_queue(), ^{ - [NowPlayingCenter updateWithItem:currentItem rate:1.0 position:position duration:_duration]; - }); - }); - } - - - - playerInfo = nil; - player = nil; - } else { - NSLog(@"Player: onTimeInterval skipped... reason: seeking"); - } -} - --(int) pause: (NSString *) playerId { - NSLog(@"Player: pause"); - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - - [self doPause:playerId]; - int state = STATE_PAUSED; - [self notifyStateChange:playerId state:state overrideBlock:false]; - return Ok; -} - --(void) doPause:(NSString *) playerId { - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - - [ player pause ]; - [playerInfo setObject:@false forKey:@"isPlaying"]; -} - --(int) resume: (NSString *) playerId { - if ([self ensureConnected:playerId isLocal:latestIsLocal] == -1) { - return -1; - } - - NSLog(@"Player: resume"); - - [self configureRemoteCommandCenter]; - [self configureAudioSession:playerId]; - - NSMutableDictionary * playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - [player play]; - [playerInfo setObject:@true forKey:@"isPlaying"]; - int state = STATE_PLAYING; - [self notifyStateChange:playerId state:state overrideBlock:false]; - - [NowPlayingCenter setWithItem:currentItem]; - - return Ok; -} - --(void) setVolume: (float) volume - playerId: (NSString *) playerId { - NSLog(@"Player: setVolume"); - NSMutableDictionary *playerInfo = players[playerId]; - AVPlayer *player = playerInfo[@"player"]; - playerInfo[@"volume"] = @(volume); - [ player setVolume:volume ]; -} - --(void) setLooping: (bool) looping - playerId: (NSString *) playerId { - NSLog(@"Player: setLooping"); - NSMutableDictionary *playerInfo = players[playerId]; - [playerInfo setObject:@(looping) forKey:@"looping"]; -} - --(void) stop: (NSString *) playerId { - NSLog(@"Player: stop"); - NSMutableDictionary * playerInfo = players[playerId]; - - if ([playerInfo[@"isPlaying"] boolValue]) { - [ self pause:playerId ]; - [ self seek:playerId time:CMTimeMake(0, 1) ]; - [playerInfo setObject:@false forKey:@"isPlaying"]; - int state = STATE_STOPPED; - [self notifyStateChange:playerId state:state overrideBlock:false]; - } -} - --(int) seek: (NSString *) playerId - time: (CMTime) time { - if ([self ensureConnected:playerId isLocal:latestIsLocal] == -1) { - return -1; - } - NSLog(@"Player: seek"); - NSMutableDictionary * playerInfo = players[playerId]; - [playerInfo setValue:@(true) forKey:@"isSeeking"]; - AVPlayer *player = playerInfo[@"player"]; - [[player currentItem] seekToTime:time]; - [self onTimeInterval:playerId time:time]; - return Ok; -} - --(void) onSoundComplete: (NSString *) playerId { - NSLog(@"Player: ios -> onSoundComplete..."); - if(!alreadyhasEnded){ - alreadyhasEnded = true; - [ _channel_player invokeMethod:@"audio.onComplete" arguments:@{@"playerId": playerId}]; - } -} - --(void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context { - NSLog(@"Player: observeValueForKeyPath: %@", keyPath); - if ([keyPath isEqualToString: @"status"]) { - NSMutableDictionary * playerInfo = players[_playerId]; - AVPlayer *player = playerInfo[@"player"]; - - NSLog(@"Player: player status: %ld",(long)[[player currentItem] status ]); - - // Do something with the status... - if ([[player currentItem] status ] == AVPlayerItemStatusReadyToPlay) { - [self updateDuration:_playerId]; - - VoidCallback onReady = playerInfo[@"onReady"]; - if (onReady != nil) { - [playerInfo removeObjectForKey:@"onReady"]; - onReady(_playerId); - } - } else if ([[player currentItem] status ] == AVPlayerItemStatusFailed) { - AVAsset *currentPlayerAsset = [[player currentItem] asset]; - - if ([currentPlayerAsset isKindOfClass:AVURLAsset.class]) { - NSLog(@"Player: Error.URL: %@", [(AVURLAsset *)currentPlayerAsset URL]); - } - NSLog(@"Player: Error: %@", [[player currentItem] error]); - NSLog(@"Player: PlayerError: %@", [player error]); - AVPlayerItemErrorLog *errorLog = [[player currentItem] errorLog]; - NSLog(@"Player: errorLog: %@", errorLog); - NSLog(@"Player: errorLog: events: %@", [errorLog events]); - NSLog(@"Player: errorLog: extendedLogData: %@", [errorLog extendedLogData]); - - [self disposePlayerItem:[player currentItem]]; - [self notifyOnError:_playerId errorType:PLAYER_ERROR_FAILED]; - } else { - NSLog(@"Player: player status: %ld",(long)[[player currentItem] status ]); - NSLog(@"Player: Unknown Error: %@", [[player currentItem] error]); - NSLog(@"Player: Unknown PlayerError: %@", [player error]); - AVAsset *currentPlayerAsset = [[player currentItem] asset]; - - if ([currentPlayerAsset isKindOfClass:AVURLAsset.class]) { - NSLog(@"Player: Unknown Error.URL: %@", [(AVURLAsset *)currentPlayerAsset URL]); - } - AVPlayerItemErrorLog *errorLog = [[player currentItem] errorLog]; - NSLog(@"Player: Unknown errorLog: %@", errorLog); - NSLog(@"Player: Unknown errorLog: events: %@", [errorLog events]); - NSLog(@"Player: Unknown errorLog: extendedLogData: %@", [errorLog extendedLogData]); - - [self disposePlayerItem:[player currentItem]]; - // [self notifyOnError:_playerId errorType:PLAYER_ERROR_UNKNOWN]; - } - } else if ([keyPath isEqualToString: @"playbackBufferEmpty"]) { - NSMutableDictionary * playerInfo = players[_playerId]; - AVPlayer *player = playerInfo[@"player"]; - if (player.rate != 0) { - int state = isConnected ? STATE_BUFFER_EMPTY : STATE_BUFFERING; - [self notifyStateChange:_playerId state:state overrideBlock:false]; - } else { - NSLog(@"Player: playbackBufferEmpty rate == 0"); - int state = STATE_PAUSED; - [self notifyStateChange:_playerId state:state overrideBlock:false]; - } - } else if ([keyPath isEqualToString: @"playbackLikelyToKeepUp"] || [keyPath isEqualToString: @"playbackBufferFull"]) { - NSMutableDictionary * playerInfo = players[_playerId]; - AVPlayer *player = playerInfo[@"player"]; - NSNumber* newValue = [change objectForKey:NSKeyValueChangeNewKey]; - BOOL shouldStartPlaySoon = [newValue boolValue]; - NSLog(@"Player: observeValueForKeyPath: %@ -- shouldStartPlaySoon: %s player.rate = %.2f shouldAutoStart = %s loadOnly = %s", keyPath, shouldStartPlaySoon ? "YES": "NO", player.rate, shouldAutoStart ? "YES": "NO", loadOnly? "YES": "NO"); - if (shouldStartPlaySoon && player.rate == 0 && shouldAutoStart && !loadOnly) { - player.rate = 1.0; - } - if (shouldStartPlaySoon && player.rate != 0) { - [ playerInfo setObject:@true forKey:@"isPlaying" ]; - int state = STATE_PLAYING; - [self notifyStateChange:_playerId state:state overrideBlock:false]; - } - shouldAutoStart = false; - } else { - // Any unrecognized context must belong to super - [super observeValueForKeyPath:keyPath - ofObject:object - change:change - context:context]; - } -} - -- (void) notifyStateChange:(NSString *) playerId - state:(int)state - overrideBlock:(bool)overrideBlock -{ - if (shallSendEvents || overrideBlock) { - [_channel_player invokeMethod:@"state.change" arguments:@{@"playerId": playerId, @"state": @(state)}]; - } -} - -- (void) notifyOnError:(NSString *) playerId - errorType:(int)errorType -{ - if (shallSendEvents) { - [_channel_player invokeMethod:@"audio.onError" arguments:@{@"playerId": playerId, @"errorType": @(errorType)}]; - } -} - -- (void) disposePlayer { - for (id value in timeobservers) - [value[@"player"] removeTimeObserver:value[@"observer"]]; - timeobservers = nil; - - for (NSString* playerId in players) { - NSMutableDictionary * playerInfo = players[playerId]; - NSMutableSet * observers = playerInfo[@"observers"]; - for (id ob in observers) - [[NSNotificationCenter defaultCenter] removeObserver:ob]; - } - - NSMutableDictionary * playerInfo = players[_playerId]; - AVPlayer *player = playerInfo[@"player"]; - @autoreleasepool { - [player replaceCurrentItemWithPlayerItem:nil]; - } - player = nil; - [players removeAllObjects]; - [playersCurrentItem removeAllObjects]; -} - -- (void)dealloc { - NSLog(@"Player: DEALLOC:"); - [self disposePlayerItem:latestPlayerItemObserved]; - [self disposePlayer]; - - players = nil; - playersCurrentItem = nil; - _playerId = nil; - currentResourceLoadingRequest = nil; - currentResourceLoader = nil; - serialQueue = nil; - playerQueue = nil; - timeobservers = nil; - alreadyInAudioSession = false; - isLoadingComplete = false; - latestUrl = nil; - latestIsLocal = NO; - latestCookie = nil; - latestPlayerId = nil; - latestOnReady = nil; - latestPlayerItemObserved = nil; - - [[AFNetworkReachabilityManager sharedManager] stopMonitoring]; - - [[UIApplication sharedApplication] endReceivingRemoteControlEvents]; -} - -@end - - diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 45a476b0..6e24ed3b 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -234,7 +234,6 @@ class Player { _queue.previousPosition; List get items => _queue.items; - List get isarItems => _queue.isarItems; int get size => items.length; int get currentIndex => _queue.index; @@ -418,7 +417,7 @@ class Player { state = PlayerState.STOPPED; _notifyPlayerStatusChangeEvent(EventType.RELEASED); } - // _queue.dispose(); + _queue.dispose(); return result; } @@ -458,24 +457,10 @@ class Player { } } - // static Future _handleOnComplete(Player player) async { - // player.state = PlayerState.COMPLETED; - // _notifyPlayerStateChangeEvent(player, EventType.FINISHED_PLAYING, ""); - // switch (player.repeatMode) { - // case RepeatMode.REPEAT_MODE_OFF: - // case RepeatMode.REPEAT_MODE_ALL: - // player._doNext(shallNotify: false); - // break; - - // case RepeatMode.REPEAT_MODE_ONE: - // player.rewind(); - // break; - // } - // } - static Future _doHandlePlatformCall(MethodCall call) async { final currentMedia = _queue.current; final currentIndex = _queue.index; + print('call.arguments: ${call.arguments}'); final Map callArgs = call.arguments as Map; if (call.method != 'audio.onCurrentPosition') { _log('_platformCallHandler call ${call.method} $callArgs'); diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index c24a67c2..e0bc11a5 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -19,13 +19,14 @@ class Queue { itemsReady = !initializeIsar; _initialize(); } - List isarItems = []; Future _initialize() async { if (!itemsReady) { try { - isarItems = await previousItems; + final items = await previousItems; previousIndex = await previousPlaylistIndex; previousPosition = await _previousPlaylistPosition; + int i = 0; + storage.addAll(items.map((e) => QueueItem(i++, i, e))); } catch (_) { } finally { itemsReady = true; diff --git a/packages/player/pubspec.yaml b/packages/player/pubspec.yaml index 6e763b94..6296708d 100644 --- a/packages/player/pubspec.yaml +++ b/packages/player/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: isar: *isar_version isar_flutter_libs: *isar_version path_provider: ^2.0.11 + file_picker: ^8.0.6 smaws: git: url: https://github.com/SuaMusica/flutter_plugins.git From c201a85f3bcd11783dcabe5e0b1f067108e2e058 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 15 Oct 2024 11:53:14 -0300 Subject: [PATCH 52/70] added remove_in ios --- .../br/com/suamusica/player/PlayerPlugin.kt | 3 - packages/player/example/lib/sm_player.dart | 21 ++++- .../ios/Classes/MethodChannelManager.swift | 10 +-- .../player/ios/Classes/NowPlayingCenter.swift | 1 - .../player/ios/Classes/PlayerPlugin.swift | 8 +- packages/player/ios/Classes/SMPlayer.swift | 85 +++++++++++++------ .../player/ios/Classes/SMPlayerNotifier.swift | 42 --------- packages/player/lib/src/player.dart | 51 ++++++----- packages/player/lib/src/queue.dart | 62 +++++++------- 9 files changed, 138 insertions(+), 145 deletions(-) delete mode 100644 packages/player/ios/Classes/SMPlayerNotifier.swift 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 b3687fc7..d763c664 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 @@ -34,11 +34,9 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val FAVORITE: String = "favorite" // Method names - const val LOAD_METHOD = "load" const val PLAY_METHOD = "play" const val SET_REPEAT_MODE = "set_repeat_mode" const val ENQUEUE = "enqueue" - const val ENQUEUE_ONE = "enqueue_one" const val REMOVE_ALL = "remove_all" const val REMOVE_IN = "remove_in" const val REORDER = "reorder" @@ -60,7 +58,6 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val GET_CURRENT_POSITION_METHOD = "getCurrentPosition" const val SET_RELEASE_MODE_METHOD = "setReleaseMode" const val CAN_PLAY = "can_play" - const val SEND_NOTIFICATION = "send_notification" const val DISABLE_NOTIFICATION_COMMANDS = "disable_notification_commands" const val ENABLE_NOTIFICATION_COMMANDS = "enable_notification_commands" const val TAG = "Player" diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 640d17a6..3a8dda19 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -176,7 +176,7 @@ class _SMPlayerState extends State { listOfMedias.addAll([media1, media2, media3, media4]); // } - player.enqueueAll(listOfMedias, autoPlay: true); + player.enqueueAll(listOfMedias, autoPlay: false); if (!mounted) return; @@ -224,7 +224,7 @@ class _SMPlayerState extends State { } } else if (_player.state == PlayerState.BUFFERING && _player.currentMedia != null) { - int result = await _player.resume(); + int result = await _player.play(); if (result == Player.Ok) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Audio is now playing!!!!'))); @@ -236,7 +236,7 @@ class _SMPlayerState extends State { .showSnackBar(SnackBar(content: Text('Audio is now paused!!!!'))); } } else if (_player.state == PlayerState.PAUSED) { - int result = await _player.resume(); + int result = await _player.play(); if (result == Player.Ok) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Audio is now playing again!!!!'))); @@ -358,11 +358,26 @@ class _SMPlayerState extends State { }, tooltip: 'Add media', ), + IconButton( + icon: Icon(Icons.queue), + onPressed: () { + _player.enqueueAll( + [media1, media2, media3, media4], + ); + }, + tooltip: 'Add media', + ), IconButton( icon: Icon(Icons.folder), onPressed: pickLocalFile, tooltip: 'Add local file', ), + IconButton( + icon: Icon(Icons.play_arrow), + onPressed: () => _player.playFromQueue(1, + loadOnly: true, position: Duration(seconds: 50)), + tooltip: 'Play from queue', + ), ], ), Stack( diff --git a/packages/player/ios/Classes/MethodChannelManager.swift b/packages/player/ios/Classes/MethodChannelManager.swift index f34b56a0..e1feef3d 100644 --- a/packages/player/ios/Classes/MethodChannelManager.swift +++ b/packages/player/ios/Classes/MethodChannelManager.swift @@ -30,15 +30,6 @@ public class MethodChannelManager:NSObject{ channel?.invokeMethod("audio.onCurrentPosition", arguments: args) } - func notifyNetworkStatus( - status:Bool) { - let args = MethodChannelManagerArgsBuilder() - .playerId(id:"smplayer") - .build() - channel?.invokeMethod("network.onChange", arguments: status ? "CONNECTED" : "DISCONNECTED") - } - - func notifyPlayerStateChange(state: PlayerState) { print("#CheckListeners - notifyPlayerStateChange \(state)") let args = MethodChannelManagerArgsBuilder() @@ -58,6 +49,7 @@ public class MethodChannelManager:NSObject{ } func currentMediaIndex(index: Int) { + print("#CheckListeners - currentMediaIndex \(index)") let args = MethodChannelManagerArgsBuilder() .playerId(id:"smplayer") .currentIndex(index:index) diff --git a/packages/player/ios/Classes/NowPlayingCenter.swift b/packages/player/ios/Classes/NowPlayingCenter.swift index 990b0445..283a2ca6 100644 --- a/packages/player/ios/Classes/NowPlayingCenter.swift +++ b/packages/player/ios/Classes/NowPlayingCenter.swift @@ -55,7 +55,6 @@ public class NowPlayingCenter : NSObject { nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Float(position) nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0 - nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo } } diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index a59c06df..f9d56c2c 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -70,6 +70,10 @@ public class PlayerPlugin: NSObject, FlutterPlugin { case "repeat_mode": smPlayer?.toggleRepeatMode() result(NSNumber(value: 1)) + case "remove_in": + let args = call.arguments as? [String: Any] + smPlayer?.removeByPosition(indexes:args?["indexesToDelete"] as? [Int] ?? []) + result(NSNumber(value: 1)) case "reorder": if let args = call.arguments as? [String: Any], let oldIndex = args["oldIndex"] as? Int, @@ -80,8 +84,8 @@ public class PlayerPlugin: NSObject, FlutterPlugin { result(NSNumber(value: true)) case "seek": if let args = call.arguments as? [String: Any] { - let position = args["position"] ?? 0 - smPlayer?.seek(position:position as! Int) + let position = args["position"] as? Int ?? 0 + smPlayer?.seekToPosition(position: position) } result(NSNumber(value: 1)) default: diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index bbe4f218..8a37814e 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -17,16 +17,17 @@ public class SMPlayer : NSObject { private var isShuffleModeEnabled: Bool = false var shuffledQueue: [AVPlayerItem] = [] private var listeners: SMPlayerListeners? = nil - + private var seekToLoadOnly: Bool = false + var fullQueue: [AVPlayerItem] { return historyQueue + smPlayer.items() + futureQueue } - var currentIndex : Int? { + var currentIndex : Int { guard let currentItem = smPlayer.currentItem else { - return nil + return 0 } - return fullQueue.firstIndex(of: currentItem) + return fullQueue.firstIndex(of: currentItem) ?? 0 } init(methodChannelManager: MethodChannelManager?) { @@ -35,14 +36,19 @@ public class SMPlayer : NSObject { self.methodChannelManager = methodChannelManager listeners = SMPlayerListeners(smPlayer:smPlayer,methodChannelManager:methodChannelManager) listeners?.addPlayerObservers() - listeners?.onMediaChanged = { + listeners?.onMediaChanged = { [self] in if(self.smPlayer.items().count > 0){ if(self.smPlayer.currentItem != self.fullQueue.first && self.historyQueue.count > 0){ methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) self.updateEndPlaybackObserver() + + seekToLoadOnly = !seekToLoadOnly + if(seekToLoadOnly){ + seekToLoadOnly = false + methodChannelManager?.currentMediaIndex(index: self.currentIndex) + } + self.listeners?.addItemsObservers() } - methodChannelManager?.currentMediaIndex(index: self.historyQueue.count) - self.listeners?.addItemsObservers() } } setupNowPlayingInfoCenter() @@ -72,21 +78,17 @@ public class SMPlayer : NSObject { ) } } - + func updateEndPlaybackObserver() { removeEndPlaybackObserver() addEndPlaybackObserver() } - + func disableRepeatMode() { smPlayer.repeatMode = .REPEAT_MODE_OFF methodChannelManager?.repeatModeChanged(repeatMode: smPlayer.repeatModeIndex) } - func seek(position:Int){ - let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: 60000) - smPlayer.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero) - } func toggleRepeatMode() { switch smPlayer.repeatMode { @@ -133,6 +135,20 @@ public class SMPlayer : NSObject { } } + func removeByPosition(indexes: [Int]) { + if(indexes.count > 0){ + let sortedIndexes = indexes.sorted(by: >) + var queueAfterRemovedItems = isShuffleModeEnabled ? shuffledQueue : fullQueue + for index in sortedIndexes { + if index < queueAfterRemovedItems.count { + queueAfterRemovedItems.remove(at: index) + } + } + distributeItemsInRightQueue(currentQueue: queueAfterRemovedItems, keepFirst: true) + printStatus(from: "removeByPosition") + } + } + func toggleShuffle(positionsList: [[String: Int]]) { isShuffleModeEnabled.toggle() if isShuffleModeEnabled { @@ -149,8 +165,8 @@ public class SMPlayer : NSObject { } func fillShuffledQueue() { - shuffledQueue.removeAll() - for index in shuffledIndices { + shuffledQueue.removeAll() + for index in shuffledIndices { if index < fullQueue.count { shuffledQueue.append(fullQueue[index]) } @@ -207,8 +223,7 @@ public class SMPlayer : NSObject { private func insertIntoPlayerIfNeeded() { let maxTotalItems = 5 - let currentItemCount = smPlayer.items().count - let itemsToAdd = min(maxTotalItems - currentItemCount, futureQueue.count) + let itemsToAdd = min(maxTotalItems - smPlayer.items().count, futureQueue.count) for _ in 0.. \(String(describing: smPlayer.currentItem?.playlistItem?.title))") printStatus(from:"insertIntoPlayerIfNeeded") } @@ -238,7 +254,11 @@ public class SMPlayer : NSObject { func seekToPosition(position:Int){ let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: 60000) - smPlayer.currentItem?.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: nil) + smPlayer.currentItem?.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { completed in + if completed { + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.seekEnd) + } + } ) } func getCurrentPlaylistItem() -> PlaylistItem? { @@ -248,7 +268,7 @@ public class SMPlayer : NSObject { return currentItem.playlistItem } - private func distributeItemsInRightQueue(currentQueue: [AVPlayerItem], keepFirst: Bool = true, positionArg: Int = -1) { + private func distributeItemsInRightQueue(currentQueue: [AVPlayerItem], keepFirst: Bool = true, positionArg: Int = -1, completionHandler completion: (() -> Void)? = nil) { guard currentQueue.count > 0 else { return } if(!keepFirst){ guard positionArg >= 0 else { return } @@ -278,12 +298,21 @@ public class SMPlayer : NSObject { } } insertIntoPlayerIfNeeded() + completion?() } func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false) { - distributeItemsInRightQueue(currentQueue:fullQueue, keepFirst: false, positionArg: position) - methodChannelManager?.currentMediaIndex(index: self.historyQueue.count) - seekToPosition(position: timePosition) + if (loadOnly) { + seekToLoadOnly = true + } + print("#NATIVE LOGS playFromQueue ==> position: \(position) timePosition: \(timePosition) | loadOnly \(loadOnly)") + distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position, completionHandler: { + print("#NATIVE LOGS ==> completionHandler") + self.methodChannelManager?.currentMediaIndex(index: position) + if(timePosition > 0){ + self.seekToPosition(position: timePosition) + } + }) if(loadOnly){ pause() }else{ @@ -325,7 +354,7 @@ public class SMPlayer : NSObject { } func printStatus(from:String) { - //TODO: adicionar variavel de debug + //TODO: adicionar variavel de debug if(true){ print("#printStatus #################################################") print("#printStatus \(from) ") @@ -394,7 +423,7 @@ public extension AVQueuePlayer { return currentRepeatmode } set(mode) { - + currentRepeatmode = mode switch mode { @@ -406,11 +435,11 @@ public extension AVQueuePlayer { actionAtItemEnd = .pause } } - + } - var repeatModeIndex: Int { - return RepeatMode.allCases.firstIndex(of: repeatMode) ?? -1 - } + var repeatModeIndex: Int { + return RepeatMode.allCases.firstIndex(of: repeatMode) ?? -1 + } } diff --git a/packages/player/ios/Classes/SMPlayerNotifier.swift b/packages/player/ios/Classes/SMPlayerNotifier.swift deleted file mode 100644 index c0b381fa..00000000 --- a/packages/player/ios/Classes/SMPlayerNotifier.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// SMPlayerNotifier.swift -// smplayer -// -// Created by Lucas Tonussi on 26/09/24. -// -private let tag = "SMPlayerNotifier" - -import Foundation -public class SMPlayerNotifier : NSObject{ - - let methodChannelManager: MethodChannelManager? - init(methodChannelManager: MethodChannelManager?) { - self.methodChannelManager = methodChannelManager - } - - func notifyPositionChange(position: Double, duration: Double, currentMediaIndex: Int) { - if (duration >= 0 && position >= 0) { - methodChannelManager?.notifyPositionChange(position:position, duration:duration) - } - } - -// func notifyPlayerStateChange( state: OnePlayerState, -// error: String? = nil, -// currentMediaIndex: Int, -// itsAdTime: Bool?){ -// methodChannelManager.notifyPlayerStateChange(state: state, currentMediaIndex: currentMediaIndex, itsAdTime: itsAdTime) -// } - -// func repeatModeChanged(repeatMode:Int){ -// methodChannelManager?.repeatModeChanged(repeatMode: repeatMode) -// } -// -// func onAds(status: String,code: String?, message: String?, isAudioAd: Bool = false){ -// methodChannelManager>.onAds(status: status, code: code, message: message, isAudioAd: isAudioAd) -// } -// -// func shuffleChanged(shuffleIsActive: Bool){ -// methodChannelManager?.shuffleChanged(shuffleIsActive: shuffleIsActive) -// } - -} diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 6e24ed3b..14e504fd 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -25,7 +25,11 @@ class Player { this.initializeIsar = false, this.autoPlay = false, }) { - _queue = Queue(initializeIsar: this.initializeIsar); + _queue = Queue( + initializeIsar: this.initializeIsar, + onInitialize: () { + enqueueAll(items, alreadyAddedToStorage: true); + }); player = this; } static const Ok = 1; @@ -76,7 +80,8 @@ class Player { EventType.PAUSED, EventType.PLAYING, EventType.EXTERNAL_RESUME_REQUESTED, - EventType.EXTERNAL_PAUSE_REQUESTED + EventType.EXTERNAL_PAUSE_REQUESTED, + EventType.SET_CURRENT_MEDIA_INDEX ]; Stream? _stream; @@ -118,8 +123,12 @@ class Player { Future enqueueAll( List items, { bool autoPlay = false, + bool saveOnTop = false, + bool alreadyAddedToStorage = false, }) async { - _queue.addAll(items); + if (!alreadyAddedToStorage) { + _queue.addAll(items, saveOnTop: saveOnTop); + } _cookies = await cookieSigner(); String cookie = _cookies!.toHeaders(); final int batchSize = 80; @@ -329,8 +338,13 @@ class Player { .then((result) => result); } - Future previous() async { + Future previous({bool isFromChromecast = false}) async { Media? media = _queue.possiblePrevious(); + print('possiblePrevious media: ${media?.name ?? 'null'}'); + if (isFromChromecast && media != null) { + _queue.lastPrevious = DateTime.now(); + return _queue.items.indexOf(media); + } if (media == null) { return null; } @@ -341,28 +355,22 @@ class Player { return Ok; } - Future next({ - bool shallNotify = true, - }) async { + Future next({bool isFromChromecast = false}) async { final media = _queue.possibleNext(repeatMode); + if (isFromChromecast && media != null) { + return _queue.items.indexOf(media); + } if (media != null) { - final mediaUrl = (await localMediaValidator?.call(media)) ?? media.url; if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { setRepeatMode("all"); } - return _doNext( - shallNotify: shallNotify, - mediaUrl: mediaUrl, - ); + return _doNext(); } else { return null; } } - Future _doNext({ - bool? shallNotify, - String? mediaUrl, - }) async { + Future _doNext() async { return _channel.invokeMethod('next').then((result) => result); } @@ -398,17 +406,6 @@ class Player { return Ok; } - Future resume() async { - _notifyPlayerStatusChangeEvent(EventType.RESUME_REQUESTED); - final int result = await _invokeMethod('play'); - - if (result == Ok) { - _notifyPlayerStatusChangeEvent(EventType.RESUMED); - } - - return result; - } - Future release() async { _notifyPlayerStatusChangeEvent(EventType.RELEASE_REQUESTED); final int result = await _invokeMethod('release'); diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index e0bc11a5..2a43a808 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -14,6 +14,7 @@ class Queue { shuffler, mode, this.initializeIsar = false, + this.onInitialize, }) : _shuffler = shuffler ?? SimpleShuffler() { IsarService.instance.isarEnabled = initializeIsar; itemsReady = !initializeIsar; @@ -27,6 +28,9 @@ class Queue { previousPosition = await _previousPlaylistPosition; int i = 0; storage.addAll(items.map((e) => QueueItem(i++, i, e))); + if (items.isNotEmpty) { + onInitialize?.call(); + } } catch (_) { } finally { itemsReady = true; @@ -47,13 +51,14 @@ class Queue { final Shuffler _shuffler; final bool initializeIsar; + final VoidCallback? onInitialize; bool itemsReady = false; int previousIndex = 0; PreviousPlaylistPosition? previousPosition; var storage = >[]; PreviousPlaylistMusics? previousPlaylistMusics; DateTime? _lastPrevious; - + set lastPrevious(DateTime? value) => _lastPrevious = value; Media? get current => _current ?? (index >= 0 && index < storage.length ? storage[index].item : null); @@ -127,16 +132,13 @@ class Queue { addAll( List items, { - bool shouldRemoveFirst = false, bool saveOnTop = false, }) async { - final medias = shouldRemoveFirst ? items.sublist(1) : items; - int i = storage.length == 1 ? 0 : storage.length - 1; if (saveOnTop) { - storage.insertAll(0, _toQueueItems(medias, i)); + storage.insertAll(0, _toQueueItems(items, i)); } else { - storage.addAll(_toQueueItems(medias, i)); + storage.addAll(_toQueueItems(items, i)); } await _save(medias: items, saveOnTop: saveOnTop); @@ -286,19 +288,19 @@ class Queue { return storage.isNotEmpty && index >= 0 ? storage[index].item : null; } - Media? next() { - if (storage.length == 0) { - throw AssertionError("Queue is empty"); - } else if (storage.length > 0 && index < storage.length - 1) { - final newIndex = index + 1; - setIndex = newIndex; - var media = storage[newIndex].item; - updateIsarIndex(media.id, newIndex); - return media; - } else { - return null; - } - } + // Media? next() { + // if (storage.length == 0) { + // throw AssertionError("Queue is empty"); + // } else if (storage.length > 0 && index < storage.length - 1) { + // final newIndex = index + 1; + // setIndex = newIndex; + // var media = storage[newIndex].item; + // updateIsarIndex(media.id, newIndex); + // return media; + // } else { + // return null; + // } + // } Media? possibleNext(RepeatMode repeatMode) { if (repeatMode == RepeatMode.REPEAT_MODE_OFF || @@ -344,17 +346,17 @@ class Queue { ); } - Media? item(int pos) { - final item = storage[pos].item; - updateIsarIndex(item.id, pos); - if (storage.length == 0) { - return null; - } else if (storage.length > 0 && pos <= storage.length - 1) { - return item; - } else { - return null; - } - } + // Media? item(int pos) { + // final item = storage[pos].item; + // updateIsarIndex(item.id, pos); + // if (storage.length == 0) { + // return null; + // } else if (storage.length > 0 && pos <= storage.length - 1) { + // return item; + // } else { + // return null; + // } + // } Media restart() { setIndex = 0; From 8a392ba041d56de1af059458f2568d246238af59 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 8 Nov 2024 15:04:36 -0300 Subject: [PATCH 53/70] fix init isar --- packages/player/android/build.gradle | 2 +- .../player/MediaButtonEventHandler.kt | 22 ++++- .../br/com/suamusica/player/MediaService.kt | 55 +++++++----- .../player/MediaSessionConnection.kt | 11 ++- .../suamusica/player/PlayerChangeNotifier.kt | 5 +- .../br/com/suamusica/player/PlayerPlugin.kt | 9 ++ packages/player/example/lib/sm_player.dart | 2 - .../player/ios/Classes/PlayerPlugin.swift | 14 ++-- packages/player/ios/Classes/SMPlayer.swift | 50 +++++++---- .../ios/Classes/SMPlayerListeners.swift | 14 +--- packages/player/lib/src/player.dart | 84 ++++++++----------- packages/player/lib/src/queue.dart | 37 ++++---- packages/player/pubspec.yaml | 5 +- 13 files changed, 183 insertions(+), 127 deletions(-) diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index d376988a..96ffe242 100644 --- a/packages/player/android/build.gradle +++ b/packages/player/android/build.gradle @@ -59,7 +59,7 @@ dependencies { 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') +// implementation files('/Users/lucastonussi/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar') implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' implementation "com.google.code.gson:gson:2.10.1" 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 2f1b0b81..d7267c29 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 @@ -22,12 +22,13 @@ import androidx.media3.session.SessionCommand import androidx.media3.session.SessionResult import br.com.suamusica.player.PlayerPlugin.Companion.DISABLE_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE -import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_ONE import br.com.suamusica.player.PlayerPlugin.Companion.FAVORITE import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.ID_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY +import br.com.suamusica.player.PlayerPlugin.Companion.NEW_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST import br.com.suamusica.player.PlayerPlugin.Companion.POSITION_ARGUMENT @@ -39,6 +40,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.SET_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE +import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_MEDIA_URI import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -89,6 +91,7 @@ class MediaButtonEventHandler( add(SessionCommand("send_notification", session.token.extras)) add(SessionCommand("ads_playing", Bundle.EMPTY)) add(SessionCommand("onTogglePlayPause", Bundle.EMPTY)) + add(SessionCommand(UPDATE_MEDIA_URI, session.token.extras)) }.build() val playerCommands = @@ -201,6 +204,21 @@ class MediaButtonEventHandler( if (customCommand.customAction == "pause") { mediaService.pause() } + + if (customCommand.customAction == UPDATE_MEDIA_URI) { + val newUri = args.getString(NEW_URI_ARGUMENT) + val id = args.getInt(ID_URI_ARGUMENT) + session.player.let { + for (i in 0 until it.mediaItemCount) { + val mediaItem = it.getMediaItemAt(i) + if (mediaItem.mediaId == id.toString()) { + mediaService.updateMediaUri(i, newUri) + break + } + } + } + } + if (customCommand.customAction == UPDATE_FAVORITE) { val isFavorite = args.getBoolean(IS_FAVORITE_ARGUMENT) val id = args.getInt(ID_FAVORITE_ARGUMENT) @@ -222,7 +240,7 @@ class MediaButtonEventHandler( if (customCommand.customAction == "ads_playing") { // mediaService.player?.pause() // mediaService.adsPlaying() -// mediaService.removeNotification() + mediaService.removeNotification() } if (customCommand.customAction == ENQUEUE) { val json = args.getString("json") 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 25f8b313..20dbc58e 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 @@ -64,6 +64,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import java.io.File import java.util.Collections +import java.util.WeakHashMap import java.util.concurrent.atomic.AtomicBoolean const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" @@ -98,7 +99,7 @@ class MediaService : MediaSessionService() { private val channel = Channel>(Channel.BUFFERED) private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - var currentMedias = listOf() + private val mediaItemMediaAssociations = WeakHashMap() override fun onCreate() { super.onCreate() @@ -226,6 +227,23 @@ class MediaService : MediaSessionService() { return false } + fun updateMediaUri(index: Int, uri: String?) { +// if (index != player?.currentMediaItemIndex) { + val media = player?.getMediaItemAt(index) + media?.associatedMedia?.let { + player?.removeMediaItem(index) + player?.addMediaSource( + index, prepare( + cookie, + it, + uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL) ?: "" + ) + ) +// player?.prepare() + } +// } + } + fun toggleShuffle(positionsList: List>) { player?.shuffleModeEnabled = !(player?.shuffleModeEnabled ?: false) player?.shuffleModeEnabled?.let { @@ -238,7 +256,10 @@ class MediaService : MediaSessionService() { shuffledIndices.toIntArray(), System.currentTimeMillis() ) - Log.d(TAG, "toggleShuffle - shuffleOrder is null: ${shuffleOrder == null} | shuffledIndices: ${shuffledIndices.size} - ${player?.mediaItemCount}") + Log.d( + TAG, + "toggleShuffle - shuffleOrder is null: ${shuffleOrder == null} | shuffledIndices: ${shuffledIndices.size} - ${player?.mediaItemCount}" + ) player!!.setShuffleOrder(shuffleOrder!!) } playerChangeNotifier?.onShuffleModeEnabled(it) @@ -273,7 +294,6 @@ class MediaService : MediaSessionService() { if (player?.mediaItemCount == 0) { player?.playWhenReady = autoPlay } - currentMedias = medias addToQueue(medias) } @@ -281,7 +301,7 @@ class MediaService : MediaSessionService() { val mediaSources: MutableList = mutableListOf() if (medias.isNotEmpty()) { for (i in medias.indices) { - mediaSources.add(prepare(cookie, medias[i],"")) + mediaSources.add(prepare(cookie, medias[i], "")) } player?.addMediaSources(mediaSources) player?.prepare() @@ -309,7 +329,7 @@ class MediaService : MediaSessionService() { } val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata) .setMediaId(media.id.toString()).build() - + mediaItem.associatedMedia = media @C.ContentType val type = Util.inferContentType(uri) return when (type) { @@ -619,7 +639,7 @@ class MediaService : MediaSessionService() { ) { super.onMediaItemTransition(mediaItem, reason) Log.d(TAG, "onMediaItemTransition: reason: ${reason}") - if((player?.mediaItemCount?:0) > 0) { + if ((player?.mediaItemCount ?: 0) > 0) { playerChangeNotifier?.currentMediaIndex( currentIndex(), "onMediaItemTransition", @@ -667,23 +687,6 @@ class MediaService : MediaSessionService() { "onPlayerError cause ${error.cause.toString()}" ) - if (error.cause.toString() - .contains("No such file or directory") - ) { - val mediaItem = player?.currentMediaItem!! - player?.removeMediaItem(player?.currentMediaItemIndex ?: 0) - player?.addMediaSource( - player?.currentMediaItemIndex ?: 0, prepare( - cookie, - currentMedias[player?.currentMediaItemIndex ?: 0], - mediaItem.mediaMetadata.extras?.getString(FALLBACK_URL) ?: "" - ) - ) - player?.prepare() - playFromQueue(currentIndex() - 1, 0) - return - } - playerChangeNotifier?.notifyError( if (error.cause.toString() .contains("Permission denied") @@ -696,6 +699,12 @@ class MediaService : MediaSessionService() { } } + var MediaItem.associatedMedia: Media? + get() = mediaItemMediaAssociations[this] + set(value) { + mediaItemMediaAssociations[this] = value + } + private inner class ProgressTracker(val handler: Handler) : Runnable { private val shutdownRequest = AtomicBoolean(false) private var shutdownTask: (() -> Unit)? = null 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 bf469263..d44a12ae 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 @@ -11,11 +11,12 @@ import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import br.com.suamusica.player.PlayerPlugin.Companion.DISABLE_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE -import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_ONE import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.ID_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY +import br.com.suamusica.player.PlayerPlugin.Companion.NEW_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST import br.com.suamusica.player.PlayerPlugin.Companion.POSITION_ARGUMENT @@ -27,6 +28,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.SET_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE +import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_MEDIA_URI import com.google.gson.Gson import java.lang.ref.WeakReference @@ -117,6 +119,13 @@ class MediaSessionConnection( sendCommand(UPDATE_FAVORITE, bundle) } + fun updateMediaUri(id:Int,newUri:String?){ + val bundle = Bundle() + bundle.putString(NEW_URI_ARGUMENT,newUri) + bundle.putInt(ID_URI_ARGUMENT,id) + sendCommand(UPDATE_MEDIA_URI, bundle) + } + fun removeAll() { sendCommand(REMOVE_ALL, null) } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index ef782041..8aaae546 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -23,6 +23,7 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { } fun notifyError(message: String? = null){ + Log.i("Player", "Notifying Error: $message") channelManager.notifyError("sua-musica-player", PlayerState.ERROR, message) } @@ -40,11 +41,11 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { channelManager.notifyPrevious("sua-musica-player") } fun notifyItemTransition(from:String) { - Log.i("Player", "notifyItemTransition") + Log.i("Player", "#NATIVE LOGS ==> notifyItemTransition | FROM: $from") channelManager.notifyItemTransition("sua-musica-player") } fun currentMediaIndex(currentMediaIndex: Int, from: String) { - Log.i("Player", "=>> currentMediaIndex | FROM: $from | $currentMediaIndex") + Log.i("Player", "#NATIVE LOGS ==> currentMediaIndex | FROM: $from | $currentMediaIndex") channelManager.currentMediaIndex("sua-musica-player", currentMediaIndex) } fun notifyPositionChange(position: Long, duration: Long) { 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 d763c664..3c1be009 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,8 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val IS_FAVORITE_ARGUMENT = "isFavorite" const val FALLBACK_URL = "fallbackURL" const val ID_FAVORITE_ARGUMENT = "idFavorite" + const val NEW_URI_ARGUMENT = "newUri" + const val ID_URI_ARGUMENT = "idUri" const val POSITION_ARGUMENT = "position" const val TIME_POSITION_ARGUMENT = "timePosition" const val INDEXES_TO_REMOVE = "indexesToDelete" @@ -49,6 +51,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val REPEAT_MODE = "repeat_mode" const val DISABLE_REPEAT_MODE = "disable_repeat_mode" const val UPDATE_FAVORITE = "update_favorite" + const val UPDATE_MEDIA_URI = "update_media_uri" const val STOP_METHOD = "stop" const val RELEASE_METHOD = "release" const val SEEK_METHOD = "seek" @@ -178,6 +181,12 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { PlayerSingleton.mediaSessionConnection?.toggleShuffle(positionsList) } + UPDATE_MEDIA_URI ->{ + val id = call.argument("id") ?: 0 + val uri = call.argument("uri") + PlayerSingleton.mediaSessionConnection?.updateMediaUri(id,uri) + } + REPEAT_MODE -> { PlayerSingleton.mediaSessionConnection?.repeatMode() } diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 3a8dda19..25328700 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:smaws/aws.dart'; import 'package:smplayer/player.dart'; diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index f9d56c2c..c5b96c87 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -8,9 +8,6 @@ let TAG = "SMPlayerIos" let CHANNEL = "suamusica.com.br/player" let CHANNEL_NOTIFICATION = "One_Player_Notification" let NOTIFICATION_ID = 0xb339 -//let MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8 -//let AUDIO_MPEG = MimeTypes.BASE_TYPE_AUDIO -var registrarAds: FlutterPluginRegistrar? = nil private let tag = TAG private var smPlayer: SMPlayer? = nil @@ -20,8 +17,6 @@ public class PlayerPlugin: NSObject, FlutterPlugin { let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: registrar.messenger()) let instance = PlayerPlugin() registrar.addMethodCallDelegate(instance, channel: channel) - - registrarAds = registrar smPlayer = SMPlayer(methodChannelManager: MethodChannelManager(channel: channel)) } @@ -82,6 +77,13 @@ public class PlayerPlugin: NSObject, FlutterPlugin { smPlayer?.reorder(fromIndex: oldIndex, toIndex: newIndex,positionsList: positionsList) } result(NSNumber(value: true)) + case "update_media_uri": + if let args = call.arguments as? [String: Any], + let id = args["id"] as? Int, + let uri = args["uri"] as? String { + smPlayer?.updateMediaUri(id: id, uri: uri) + } + result(NSNumber(value: true)) case "seek": if let args = call.arguments as? [String: Any] { let position = args["position"] as? Int ?? 0 @@ -133,7 +135,9 @@ public class PlayerPlugin: NSObject, FlutterPlugin { } deinit { + print("PlayerPlugin: deinit") smPlayer?.clearNowPlayingInfo() + smPlayer?.removeAll() } } diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 8a37814e..d2fcb738 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -8,7 +8,6 @@ public class SMPlayer : NSObject { var methodChannelManager: MethodChannelManager? //Queue handle private var smPlayer: AVQueuePlayer - private var playerItem: AVPlayerItem? private var historyQueue: [AVPlayerItem] = [] private var futureQueue: [AVPlayerItem] = [] //Shuffle handle @@ -40,14 +39,13 @@ public class SMPlayer : NSObject { if(self.smPlayer.items().count > 0){ if(self.smPlayer.currentItem != self.fullQueue.first && self.historyQueue.count > 0){ methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) - self.updateEndPlaybackObserver() - - seekToLoadOnly = !seekToLoadOnly - if(seekToLoadOnly){ - seekToLoadOnly = false - methodChannelManager?.currentMediaIndex(index: self.currentIndex) - } - self.listeners?.addItemsObservers() + } + self.updateEndPlaybackObserver() + seekToLoadOnly = !seekToLoadOnly + self.listeners?.addItemsObservers() + if(seekToLoadOnly){ + seekToLoadOnly = false + methodChannelManager?.currentMediaIndex(index: self.currentIndex) } } } @@ -114,6 +112,7 @@ public class SMPlayer : NSObject { } func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { + var playerItem: AVPlayerItem? guard let message = MessageBuffer.shared.receive() else { return } let isFirstBatch = self.smPlayer.items().count == 0 for media in message { @@ -270,9 +269,9 @@ public class SMPlayer : NSObject { private func distributeItemsInRightQueue(currentQueue: [AVPlayerItem], keepFirst: Bool = true, positionArg: Int = -1, completionHandler completion: (() -> Void)? = nil) { guard currentQueue.count > 0 else { return } - if(!keepFirst){ - guard positionArg >= 0 else { return } - } + // if(!keepFirst){ + // guard positionArg >= 0 else { return } + // } var position = positionArg historyQueue.removeAll() futureQueue.removeAll() @@ -301,14 +300,37 @@ public class SMPlayer : NSObject { completion?() } + func updateMediaUri(id: Int, uri: String?){ + var fullQueueUpdated = fullQueue + if let index = fullQueue.firstIndex(where: { $0.playlistItem?.mediaId == id }){ + let oldItem = fullQueueUpdated[index] + var playerItem: AVPlayerItem? + if(uri?.contains("https") ?? true){ + guard let url = URL(string: (uri ?? oldItem.playlistItem!.fallbackUrl!)) else { return } + let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": oldItem.playlistItem?.cookie]] + playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) + }else{ + playerItem = AVPlayerItem(asset:AVAsset(url: NSURL(fileURLWithPath: uri!) as URL)) + } + playerItem?.playlistItem = oldItem.playlistItem + fullQueueUpdated[index] = playerItem! + print("updateMediaUri: \(String(describing: uri))") + for item in fullQueueUpdated { + print("#updateMediaUri QUEUE: \(String(describing: item.playlistItem?.title)) | \(item.asset) | \(currentIndex)") + } + distributeItemsInRightQueue(currentQueue: fullQueueUpdated) + } + + } + func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false) { if (loadOnly) { seekToLoadOnly = true } - print("#NATIVE LOGS playFromQueue ==> position: \(position) timePosition: \(timePosition) | loadOnly \(loadOnly)") + listeners?.removeItemObservers() distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position, completionHandler: { print("#NATIVE LOGS ==> completionHandler") - self.methodChannelManager?.currentMediaIndex(index: position) + self.methodChannelManager?.currentMediaIndex(index: self.currentIndex) if(timePosition > 0){ self.seekToPosition(position: timePosition) } diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index dcd1af14..83d858e5 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -27,20 +27,8 @@ public class SMPlayerListeners : NSObject { removeItemObservers() guard let currentItem = smPlayer.currentItem else { return } statusChange = currentItem.observe(\.status, options: [.new, .old]) { (playerItem, change) in - if playerItem.status == .readyToPlay { - print("#Listeners - readyToPlay") - } else if playerItem.status == .failed { + if playerItem.status == .failed { if let error = playerItem.error { - print("#Listeners notifyError \(error.localizedDescription)") - if let fallbackUrl = playerItem.playlistItem?.fallbackUrl { - let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": playerItem.playlistItem?.cookie ?? ""]] - let fallbackItem = AVPlayerItem(asset: AVURLAsset(url: URL(string: fallbackUrl)! , options: assetOptions)) - - self.smPlayer.replaceCurrentItem(with: fallbackItem) - } else { - self.methodChannelManager?.notifyError(error: "NO FALLBACK") - } - } else { self.methodChannelManager?.notifyError(error: "UNKNOW ERROR") } } diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 14e504fd..8b806e47 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:smaws/aws.dart'; import 'package:flutter/services.dart'; -import 'package:smplayer/src/before_play_event.dart'; import 'package:smplayer/src/event.dart'; import 'package:smplayer/src/event_type.dart'; import 'package:smplayer/src/isar_service.dart'; @@ -26,10 +25,12 @@ class Player { this.autoPlay = false, }) { _queue = Queue( - initializeIsar: this.initializeIsar, - onInitialize: () { - enqueueAll(items, alreadyAddedToStorage: true); - }); + beforeInitialize: () async => await _channel.invokeMethod('remove_all'), + initializeIsar: this.initializeIsar, + onInitialize: () async { + await enqueueAll(items, alreadyAddedToStorage: true); + }, + ); player = this; } static const Ok = 1; @@ -95,19 +96,22 @@ class Player { String method, [ Map? arguments, ]) async { - arguments ??= const {}; - if (_cookies == null || !_cookies!.isValid) { - _log("Generating Cookies"); - _cookies = await cookieSigner(); - } - String cookie = _cookies!.toHeaders(); - if (method == "play") { - _log("Cookie: $cookie"); + if (!_shallSendEvents) { + return NotOk; } + arguments ??= const {}; + // if (_cookies == null || !_cookies!.isValid) { + // _log("Generating Cookies"); + // _cookies = await cookieSigner(); + // } + // String cookie = _cookies!.toHeaders(); + // if (method == "play") { + // _log("Cookie: $cookie"); + // } final Map args = Map.of(arguments) ..['playerId'] = playerId - ..['cookie'] = cookie + // ..['cookie'] = cookie ..['shallSendEvents'] = _shallSendEvents ..['externalplayback'] = externalPlayback; @@ -120,6 +124,14 @@ class Player { _queue.setIndex = position; } + Future updateMediaUri({required int id, String? uri}) async { + _channel.invokeMethod('update_media_uri', { + 'id': id, + 'uri': uri, + }); + return Ok; + } + Future enqueueAll( List items, { bool autoPlay = false, @@ -129,17 +141,21 @@ class Player { if (!alreadyAddedToStorage) { _queue.addAll(items, saveOnTop: saveOnTop); } - _cookies = await cookieSigner(); + if (_cookies == null || !_cookies!.isValid) { + _log("Generating Cookies"); + _cookies = await cookieSigner(); + } String cookie = _cookies!.toHeaders(); final int batchSize = 80; _idSum = 0; final List> batchArgs = items.map( (media) { _idSum += media.id; + final localPath = localMediaValidator?.call(media); return { ...media .copyWith( - url: localMediaValidator?.call(media) ?? media.url, + url: localPath ?? media.url, ) .toJson(), }; @@ -248,7 +264,7 @@ class Player { int get currentIndex => _queue.index; Future play({bool shouldPrepare = false}) async { - _channel.invokeMethod( + await _invokeMethod( 'play', {'shouldPrepare': shouldPrepare}, ); @@ -273,30 +289,6 @@ class Player { }).then((result) => result); } - Future _doPlay({ - bool shouldPrepare = false, - }) async { - // _notifyBeforePlayEvent( - // (_) => - _channel.invokeMethod( - 'play', - {'shouldPrepare': shouldPrepare}, - // ), - ); - return Ok; - } - - Future invokePlay(Media media, Map args) async { - print(args); - final int result = await _invokeMethod('play', args); - return result; - } - - Future invokeLoad(Map args) async { - final int result = await _invokeMethod('load', args); - return result; - } - List> getPositionsList() { return [ for (var item in _queue.storage) @@ -340,7 +332,6 @@ class Player { Future previous({bool isFromChromecast = false}) async { Media? media = _queue.possiblePrevious(); - print('possiblePrevious media: ${media?.name ?? 'null'}'); if (isFromChromecast && media != null) { _queue.lastPrevious = DateTime.now(); return _queue.items.indexOf(media); @@ -351,8 +342,7 @@ class Player { if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { setRepeatMode("all"); } - _channel.invokeMethod('previous').then((result) => result); - return Ok; + return await _invokeMethod('previous'); } Future next({bool isFromChromecast = false}) async { @@ -371,7 +361,7 @@ class Player { } Future _doNext() async { - return _channel.invokeMethod('next').then((result) => result); + return _invokeMethod('next').then((result) => result); } Future updateFavorite({ @@ -386,9 +376,7 @@ class Player { Future pause() async { _notifyPlayerStatusChangeEvent(EventType.PAUSE_REQUEST); - - // return await _invokeMethod('pause'); - return _channel.invokeMethod('pause').then((result) => result); + return await _invokeMethod('pause'); } void addUsingPlayer(Event event) => _addUsingPlayer(player, event); diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index 2a43a808..bad14c76 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -15,27 +15,34 @@ class Queue { mode, this.initializeIsar = false, this.onInitialize, + this.beforeInitialize, }) : _shuffler = shuffler ?? SimpleShuffler() { IsarService.instance.isarEnabled = initializeIsar; itemsReady = !initializeIsar; _initialize(); } Future _initialize() async { - if (!itemsReady) { - try { - final items = await previousItems; - previousIndex = await previousPlaylistIndex; - previousPosition = await _previousPlaylistPosition; - int i = 0; - storage.addAll(items.map((e) => QueueItem(i++, i, e))); - if (items.isNotEmpty) { - onInitialize?.call(); + beforeInitialize?.call().then( + (_) async { + if (!itemsReady) { + try { + final items = await previousItems; + previousIndex = await previousPlaylistIndex; + previousPosition = await _previousPlaylistPosition; + int i = 0; + storage.addAll(items.map((e) => QueueItem(i++, i, e))); + if (items.isNotEmpty) { + debugPrint('#APP LOGS ==> onInitialize'); + onInitialize?.call().then((_) => itemsReady = true); + } else { + itemsReady = true; + } + } catch (_) { + itemsReady = true; + } } - } catch (_) { - } finally { - itemsReady = true; - } - } + }, + ); } var _index = 0; @@ -51,7 +58,7 @@ class Queue { final Shuffler _shuffler; final bool initializeIsar; - final VoidCallback? onInitialize; + final Future Function()? onInitialize, beforeInitialize; bool itemsReady = false; int previousIndex = 0; PreviousPlaylistPosition? previousPosition; diff --git a/packages/player/pubspec.yaml b/packages/player/pubspec.yaml index 6296708d..adf86269 100644 --- a/packages/player/pubspec.yaml +++ b/packages/player/pubspec.yaml @@ -16,7 +16,10 @@ dependencies: isar: *isar_version isar_flutter_libs: *isar_version path_provider: ^2.0.11 - file_picker: ^8.0.6 + file_picker: + git: + url: https://github.com/SuaMusica/flutter_file_picker.git + ref: 528cd01a317b45305d293c03e855ec7255c3ab59 smaws: git: url: https://github.com/SuaMusica/flutter_plugins.git From 0ac0bad610e602cd5b588d6dad3aa1f3fc530231 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 6 Dec 2024 14:56:28 -0300 Subject: [PATCH 54/70] fix androido transition --- .../player/MediaButtonEventHandler.kt | 1 + .../br/com/suamusica/player/MediaService.kt | 99 ++++++------------- .../player/MediaSessionConnection.kt | 3 +- .../br/com/suamusica/player/PlayerPlugin.kt | 2 + packages/player/lib/src/player.dart | 2 + 5 files changed, 39 insertions(+), 68 deletions(-) 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 d7267c29..c7639034 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 @@ -251,6 +251,7 @@ class MediaButtonEventHandler( mediaService.enqueue( mediaList, args.getBoolean("autoPlay"), + args.getBoolean("shouldNotifyTransition"), ) } return Futures.immediateFuture( 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 20dbc58e..6bb35d28 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,6 +1,7 @@ package br.com.suamusica.player import android.app.ActivityManager +import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.Intent @@ -9,6 +10,7 @@ import android.os.Build import android.os.Bundle import android.os.Handler import android.support.v4.media.session.PlaybackStateCompat +import androidx.core.app.NotificationManagerCompat import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem @@ -94,8 +96,10 @@ class MediaService : MediaSessionService() { private var shuffleOrder: DefaultShuffleOrder? = null private var seekToLoadOnly: Boolean = false +// private var enqueueLoadOnly: Boolean = false private var shuffledIndices = mutableListOf() private var autoPlay: Boolean = true + private var shouldNotifyTransition: Boolean = true private val channel = Channel>(Channel.BUFFERED) private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) @@ -164,6 +168,12 @@ class MediaService : MediaSessionService() { } } + fun removeNotification() { + Log.d("Player", "removeNotification") + player?.stop() +// NotificationManagerCompat.from(applicationContext).cancelAll() + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) return Service.START_STICKY @@ -229,18 +239,18 @@ class MediaService : MediaSessionService() { fun updateMediaUri(index: Int, uri: String?) { // if (index != player?.currentMediaItemIndex) { - val media = player?.getMediaItemAt(index) - media?.associatedMedia?.let { - player?.removeMediaItem(index) - player?.addMediaSource( - index, prepare( - cookie, - it, - uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL) ?: "" - ) + val media = player?.getMediaItemAt(index) + media?.associatedMedia?.let { + player?.removeMediaItem(index) + player?.addMediaSource( + index, prepare( + cookie, + it, + uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL) ?: "" ) + ) // player?.prepare() - } + } // } } @@ -285,15 +295,22 @@ class MediaService : MediaSessionService() { fun enqueue( medias: List, autoPlay: Boolean, + shouldNotifyTransition: Boolean, ) { Log.d( TAG, - "onMediaItemTransition: mediaItemCount: ${player?.mediaItemCount} | autoPlay: $autoPlay" + "enqueue: mediaItemCount: ${player?.mediaItemCount} | autoPlay: $autoPlay" ) this.autoPlay = autoPlay + this.shouldNotifyTransition = shouldNotifyTransition if (player?.mediaItemCount == 0) { player?.playWhenReady = autoPlay } +// enqueueLoadOnly = autoPlay + android.util.Log.d( + "#NATIVE LOGS ==>", + "enqueue $autoPlay | mediaItemCount: ${player?.mediaItemCount} | shouldNotifyTransition: ${shouldNotifyTransition}" + ) addToQueue(medias) } @@ -379,10 +396,6 @@ class MediaService : MediaSessionService() { val sortedIndexes = indexes.sortedDescending() if (sortedIndexes.isNotEmpty()) { sortedIndexes.forEach { - android.util.Log.d( - "#NATIVE LOGS ==>", - "removeIn ${player?.getMediaItemAt(it)?.mediaMetadata?.title}" - ) player?.removeMediaItem(it) if (shuffledIndices.isNotEmpty()) { shuffledIndices.removeAt( @@ -474,6 +487,7 @@ class MediaService : MediaSessionService() { ) if (!loadOnly) { player?.prepare() + playerChangeNotifier?.notifyItemTransition("playFromQueue") } playerChangeNotifier?.currentMediaIndex(currentIndex(), "playFromQueue") } @@ -588,57 +602,11 @@ class MediaService : MediaSessionService() { } } - override fun onEvents(player: Player, events: Player.Events) { - super.onEvents(player, events) - for (i in 0 until events.size()) { - val event = events.get(i) - val eventName = when (event) { - Player.EVENT_TIMELINE_CHANGED -> "EVENT_TIMELINE_CHANGED" - Player.EVENT_MEDIA_ITEM_TRANSITION -> "EVENT_MEDIA_ITEM_TRANSITION" - Player.EVENT_TRACKS_CHANGED -> "EVENT_TRACKS_CHANGED" - Player.EVENT_IS_LOADING_CHANGED -> "EVENT_IS_LOADING_CHANGED" - Player.EVENT_PLAYBACK_STATE_CHANGED -> "EVENT_PLAYBACK_STATE_CHANGED" - Player.EVENT_PLAY_WHEN_READY_CHANGED -> "EVENT_PLAY_WHEN_READY_CHANGED" - Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED -> "EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED" - Player.EVENT_IS_PLAYING_CHANGED -> "EVENT_IS_PLAYING_CHANGED" - Player.EVENT_REPEAT_MODE_CHANGED -> "EVENT_REPEAT_MODE_CHANGED" - Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED -> "EVENT_SHUFFLE_MODE_ENABLED_CHANGED" - Player.EVENT_PLAYER_ERROR -> "EVENT_PLAYER_ERROR" - Player.EVENT_POSITION_DISCONTINUITY -> "EVENT_POSITION_DISCONTINUITY" - Player.EVENT_PLAYBACK_PARAMETERS_CHANGED -> "EVENT_PLAYBACK_PARAMETERS_CHANGED" - Player.EVENT_AVAILABLE_COMMANDS_CHANGED -> "EVENT_AVAILABLE_COMMANDS_CHANGED" - Player.EVENT_MEDIA_METADATA_CHANGED -> "EVENT_MEDIA_METADATA_CHANGED" - Player.EVENT_PLAYLIST_METADATA_CHANGED -> "EVENT_PLAYLIST_METADATA_CHANGED" - Player.EVENT_SEEK_BACK_INCREMENT_CHANGED -> "EVENT_SEEK_BACK_INCREMENT_CHANGED" - Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED -> "EVENT_SEEK_FORWARD_INCREMENT_CHANGED" - Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED -> "EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED" - Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED -> "EVENT_TRACK_SELECTION_PARAMETERS_CHANGED" - Player.EVENT_AUDIO_ATTRIBUTES_CHANGED -> "EVENT_AUDIO_ATTRIBUTES_CHANGED" - Player.EVENT_AUDIO_SESSION_ID -> "EVENT_AUDIO_SESSION_ID" - Player.EVENT_VOLUME_CHANGED -> "EVENT_VOLUME_CHANGED" - Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED -> "EVENT_SKIP_SILENCE_ENABLED_CHANGED" - Player.EVENT_SURFACE_SIZE_CHANGED -> "EVENT_SURFACE_SIZE_CHANGED" - Player.EVENT_VIDEO_SIZE_CHANGED -> "EVENT_VIDEO_SIZE_CHANGED" - Player.EVENT_RENDERED_FIRST_FRAME -> "EVENT_RENDERED_FIRST_FRAME" - Player.EVENT_CUES -> "EVENT_CUES" - Player.EVENT_METADATA -> "EVENT_METADATA" - Player.EVENT_DEVICE_VOLUME_CHANGED -> "EVENT_DEVICE_VOLUME_CHANGED" - Player.EVENT_DEVICE_INFO_CHANGED -> "EVENT_DEVICE_INFO_CHANGED" - else -> "UNKNOWN_EVENT" - } - Log.d(TAG, "LOG onEvents: reason: $eventName") - if (event == Player.EVENT_MEDIA_METADATA_CHANGED) { - playerChangeNotifier?.notifyPlaying(player.isPlaying) - } - } - } - override fun onMediaItemTransition( mediaItem: MediaItem?, reason: @MediaItemTransitionReason Int ) { super.onMediaItemTransition(mediaItem, reason) - Log.d(TAG, "onMediaItemTransition: reason: ${reason}") if ((player?.mediaItemCount ?: 0) > 0) { playerChangeNotifier?.currentMediaIndex( currentIndex(), @@ -646,13 +614,10 @@ class MediaService : MediaSessionService() { ) } mediaButtonEventHandler.buildIcons() - if (reason != Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) { - if (!seekToLoadOnly) { - player?.playWhenReady = true - seekToLoadOnly = false - } - playerChangeNotifier?.notifyItemTransition("onMediaItemTransition != 3 seekToLoadOnly: $seekToLoadOnly") + if(reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED || !shouldNotifyTransition){ + return } + playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: ${reason} | shouldNotifyTransition: ${shouldNotifyTransition}") } var lastState = PlaybackStateCompat.STATE_NONE - 1 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 d44a12ae..a26bb11d 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 @@ -64,10 +64,11 @@ class MediaSessionConnection( } } - fun enqueue(medias: String, autoPlay: Boolean) { + fun enqueue(medias: String, autoPlay: Boolean,shouldNotifyTransition:Boolean) { val bundle = Bundle() bundle.putString("json", medias) bundle.putBoolean("autoPlay", autoPlay) + bundle.putBoolean("shouldNotifyTransition", shouldNotifyTransition) sendCommand(ENQUEUE, bundle) } 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 3c1be009..d59f25fa 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 @@ -135,10 +135,12 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { val listMedia: List> = batch["batch"] as List> val autoPlay: Boolean = (batch["autoPlay"] ?: false) as Boolean + val shouldNotifyTransition: Boolean = (batch["shouldNotifyTransition"] ?: false) as Boolean val json = Gson().toJson(listMedia) PlayerSingleton.mediaSessionConnection?.enqueue( json, autoPlay, + shouldNotifyTransition, ) } diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 8b806e47..2072796d 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -137,6 +137,7 @@ class Player { bool autoPlay = false, bool saveOnTop = false, bool alreadyAddedToStorage = false, + bool shouldNotifyTransition = false, }) async { if (!alreadyAddedToStorage) { _queue.addAll(items, saveOnTop: saveOnTop); @@ -172,6 +173,7 @@ class Player { 'playerId': playerId, 'shallSendEvents': _shallSendEvents, 'externalplayback': externalPlayback, + 'shouldNotifyTransition': shouldNotifyTransition, if (i == 0) ...{ 'cookie': cookie, }, From c1714f4a653ab272953b8c9f6724250f47127885 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 6 Dec 2024 15:02:04 -0300 Subject: [PATCH 55/70] modifying gitignore --- packages/player/.gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/player/.gitignore b/packages/player/.gitignore index a8a202ba..2292831d 100644 --- a/packages/player/.gitignore +++ b/packages/player/.gitignore @@ -6,7 +6,9 @@ build/ - +example/linux/ +example/macos/ +example/windows/ # Created by https://www.gitignore.io/api/dart,android,flutter,jetbrains,visualstudio,androidstudio,visualstudiocode # Edit at https://www.gitignore.io/?templates=dart,android,flutter,jetbrains,visualstudio,androidstudio,visualstudiocode From 9e8c3168c463ed114e48070038b6dbdf2749f83d Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 9 Dec 2024 12:56:54 -0300 Subject: [PATCH 56/70] fix transition --- .../kotlin/br/com/suamusica/player/MediaService.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 6bb35d28..867f2405 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 @@ -309,7 +309,7 @@ class MediaService : MediaSessionService() { // enqueueLoadOnly = autoPlay android.util.Log.d( "#NATIVE LOGS ==>", - "enqueue $autoPlay | mediaItemCount: ${player?.mediaItemCount} | shouldNotifyTransition: ${shouldNotifyTransition}" + "enqueue $autoPlay | mediaItemCount: ${player?.mediaItemCount} | shouldNotifyTransition: $shouldNotifyTransition" ) addToQueue(medias) } @@ -323,10 +323,13 @@ class MediaService : MediaSessionService() { player?.addMediaSources(mediaSources) player?.prepare() // PlayerSingleton.playerChangeNotifier?.notifyItemTransition("createMediaSource") - playerChangeNotifier?.currentMediaIndex( - currentIndex(), - "createMediaSource", - ) +// playerChangeNotifier?.currentMediaIndex( +// currentIndex(), +// "createMediaSource", +// ) + } + if(shouldNotifyTransition){ + playerChangeNotifier?.notifyItemTransition("Enqueue - createMediaSource") } } @@ -489,7 +492,6 @@ class MediaService : MediaSessionService() { player?.prepare() playerChangeNotifier?.notifyItemTransition("playFromQueue") } - playerChangeNotifier?.currentMediaIndex(currentIndex(), "playFromQueue") } fun removeAll() { From 109ad7ab70ed16c8b75a13a3aa9d9874a5905428 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 9 Dec 2024 17:46:22 -0300 Subject: [PATCH 57/70] notify when change in notification --- .../kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt | 3 ++- .../src/main/kotlin/br/com/suamusica/player/MediaService.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 c7639034..1fdba275 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 @@ -192,13 +192,14 @@ class MediaButtonEventHandler( } if (customCommand.customAction == "notification_previous" || customCommand.customAction == "previous") { if(session.player.hasPreviousMediaItem()){ - session.player.seekToPreviousMediaItem() }else{ session.player.seekToPrevious() } + mediaService.shouldNotifyTransition = true } if (customCommand.customAction == "notification_next" || customCommand.customAction == "next") { + mediaService.shouldNotifyTransition = true session.player.seekToNextMediaItem() } if (customCommand.customAction == "pause") { 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 867f2405..652d6c25 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 @@ -99,7 +99,7 @@ class MediaService : MediaSessionService() { // private var enqueueLoadOnly: Boolean = false private var shuffledIndices = mutableListOf() private var autoPlay: Boolean = true - private var shouldNotifyTransition: Boolean = true + var shouldNotifyTransition: Boolean = true private val channel = Channel>(Channel.BUFFERED) private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) @@ -620,6 +620,7 @@ class MediaService : MediaSessionService() { return } playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: ${reason} | shouldNotifyTransition: ${shouldNotifyTransition}") + shouldNotifyTransition = false } var lastState = PlaybackStateCompat.STATE_NONE - 1 From ba93d738750f05860f9e92cf30e9007b6bfc064b Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 10 Dec 2024 16:26:45 -0300 Subject: [PATCH 58/70] fix transition --- .../player/ios/Classes/PlayerPlugin.swift | 3 +- packages/player/ios/Classes/SMPlayer.swift | 38 +++++++++++-------- .../ios/Classes/SMPlayerListeners.swift | 20 +++++----- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index c5b96c87..44ee738d 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -28,9 +28,10 @@ public class PlayerPlugin: NSObject, FlutterPlugin { let listMedia = batch["batch"] as? [[String: Any]] { let autoPlay = batch["autoPlay"] as? Bool ?? false let cookie = batch["cookie"] as? String ?? "" + let shouldNotifyTransition = batch["shouldNotifyTransition"] as? Bool ?? false if let mediaList = convertToMedia(mediaArray: listMedia) { MessageBuffer.shared.send(mediaList) - smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie) + smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie, shouldNotifyTransition: shouldNotifyTransition) } } result(NSNumber(value: true)) diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index d2fcb738..ffea4bac 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -17,6 +17,8 @@ public class SMPlayer : NSObject { var shuffledQueue: [AVPlayerItem] = [] private var listeners: SMPlayerListeners? = nil private var seekToLoadOnly: Bool = false + // Transition Control + private var shouldNotifyTransition: Bool = false var fullQueue: [AVPlayerItem] { return historyQueue + smPlayer.items() + futureQueue @@ -35,11 +37,13 @@ public class SMPlayer : NSObject { self.methodChannelManager = methodChannelManager listeners = SMPlayerListeners(smPlayer:smPlayer,methodChannelManager:methodChannelManager) listeners?.addPlayerObservers() + listeners?.onMediaChanged = { [self] in if(self.smPlayer.items().count > 0){ - if(self.smPlayer.currentItem != self.fullQueue.first && self.historyQueue.count > 0){ + if(self.smPlayer.currentItem != self.fullQueue.first && self.historyQueue.count > 0 && shouldNotifyTransition){ methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) } + shouldNotifyTransition = true self.updateEndPlaybackObserver() seekToLoadOnly = !seekToLoadOnly self.listeners?.addItemsObservers() @@ -111,9 +115,10 @@ public class SMPlayer : NSObject { MPNowPlayingInfoCenter.default().nowPlayingInfo = nil } - func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { + func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String, shouldNotifyTransition: Bool) { var playerItem: AVPlayerItem? guard let message = MessageBuffer.shared.receive() else { return } + self.shouldNotifyTransition = shouldNotifyTransition let isFirstBatch = self.smPlayer.items().count == 0 for media in message { if(media.url!.contains("https")){ @@ -132,6 +137,9 @@ public class SMPlayer : NSObject { self.smPlayer.play() self.setNowPlaying() } + if(shouldNotifyTransition){ + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) + } } func removeByPosition(indexes: [Int]) { @@ -378,28 +386,28 @@ public class SMPlayer : NSObject { func printStatus(from:String) { //TODO: adicionar variavel de debug if(true){ - print("#printStatus #################################################") - print("#printStatus \(from) ") - print("#printStatus Current Index: \(String(describing: currentIndex))") - print("#printStatus ------------------------------------------") - print("#printStatus History: \(historyQueue.count) items") + print("QueueActivity #################################################") + print("QueueActivity \(from) ") + print("QueueActivity Current Index: \(String(describing: currentIndex))") + print("QueueActivity ------------------------------------------") + print("QueueActivity printStatus History: \(historyQueue.count) items") for item in historyQueue { - print("#printStatus History: \(String(describing: item.playlistItem?.title))") + print("QueueActivity printStatus History: \(String(describing: item.playlistItem?.title))") } - print("#printStatus ------------------------------------------") - print("#printStatus futureQueue Items: \(futureQueue.count) items") + print("QueueActivity printStatus ------------------------------------------") + print("QueueActivity printStatus futureQueue Items: \(futureQueue.count) items") for item in futureQueue { - print("#printStatus Upcoming: \(String(describing: item.playlistItem?.title))") + print("QueueActivity printStatus Upcoming: \(String(describing: item.playlistItem?.title))") } - print("#printStatus ------------------------------------------") - print("#printStatus AVQueuePlayer items: \(smPlayer.items().count)") + print("QueueActivity printStatus ------------------------------------------") + print("QueueActivity printStatus AVQueuePlayer items: \(smPlayer.items().count)") for item in smPlayer.items() { - print("#printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") + print("QueueActivity printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") } - print("#printStatus #################################################") + print("QueueActivity printStatus #################################################") } } diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index 83d858e5..f4fcbf1c 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -36,12 +36,12 @@ public class SMPlayerListeners : NSObject { loading = currentItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old]) { [weak self] (new, old) in guard let self = self else { return } - print("#Listeners - observer - loading") + print("#NATIVE LOGS ==> Listeners - observer - loading") self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) } loaded = currentItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { (player, _) in - print("#Listeners - observer - loaded") + print("#NATIVE LOGS ==> Listeners - observer - loaded") } } @@ -51,7 +51,7 @@ public class SMPlayerListeners : NSObject { mediaChange = smPlayer.observe(\.currentItem, options: [.new, .old]) { [weak self] (player, change) in guard let self = self else { return } let oldItemExists = change.oldValue != nil - print("onMediaChanged: \(oldItemExists)") + print("#NATIVE LOGS ==> onMediaChanged: \(oldItemExists)") if let newItem = change.newValue, newItem != change.oldValue { self.onMediaChanged?() @@ -75,13 +75,13 @@ public class SMPlayerListeners : NSObject { notPlayingReason = smPlayer.observe(\.reasonForWaitingToPlay, options: [.new]) { (playerItem, change) in switch self.smPlayer.reasonForWaitingToPlay { case .evaluatingBufferingRate: - print("#Listeners reasonForWaitingToPlay - evaluatingBufferingRate") + print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - evaluatingBufferingRate") case .toMinimizeStalls: - print("#Listeners reasonForWaitingToPlay - toMinimizeStalls") + print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - toMinimizeStalls") case .noItemToPlay: - print("#Listeners reasonForWaitingToPlay - noItemToPlay") + print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - noItemToPlay") default: - print("#Listeners reasonForWaitingToPlay - default") + print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - default") } } @@ -90,13 +90,13 @@ public class SMPlayerListeners : NSObject { switch player.timeControlStatus { case .playing: self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.playing) - print("#Listeners - Playing") + print("#NATIVE LOGS ==> Listeners - Playing") case .paused: self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.paused) - print("#Listeners - Paused") + print("#NATIVE LOGS ==> Listeners - Paused") case .waitingToPlayAtSpecifiedRate: self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) - print("#Listeners - Buffering") + print("#NATIVE LOGS ==> Listeners - Buffering") @unknown default: break } From 569bc44cb0bfdf3cdbcd3c0e1352d74df7270649 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 11 Dec 2024 11:28:12 -0300 Subject: [PATCH 59/70] remove comment --- packages/player/ios/Classes/SMPlayer.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index ffea4bac..0e3d763c 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -277,9 +277,6 @@ public class SMPlayer : NSObject { private func distributeItemsInRightQueue(currentQueue: [AVPlayerItem], keepFirst: Bool = true, positionArg: Int = -1, completionHandler completion: (() -> Void)? = nil) { guard currentQueue.count > 0 else { return } - // if(!keepFirst){ - // guard positionArg >= 0 else { return } - // } var position = positionArg historyQueue.removeAll() futureQueue.removeAll() From 26768bc2850085e9a60b18b4f7314f8b24057f20 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 13 Dec 2024 12:26:43 -0300 Subject: [PATCH 60/70] fix transition and repeat mode --- packages/player/ios/Classes/SMPlayer.swift | 4 ++++ packages/player/lib/src/player.dart | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 0e3d763c..9c64e744 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -192,6 +192,10 @@ public class SMPlayer : NSObject { if let currentItem = smPlayer.currentItem { historyQueue.append(currentItem) } + + if(smPlayer.currentItem == fullQueue.last && smPlayer.repeatMode == .REPEAT_MODE_ALL){ + playFromQueue(position: 0) + } smPlayer.advanceToNextItem() seekToPosition(position: 0) setNowPlaying() diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 2072796d..127e1def 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -28,7 +28,11 @@ class Player { beforeInitialize: () async => await _channel.invokeMethod('remove_all'), initializeIsar: this.initializeIsar, onInitialize: () async { - await enqueueAll(items, alreadyAddedToStorage: true); + await enqueueAll( + items, + alreadyAddedToStorage: true, + shouldNotifyTransition: false, + ); }, ); player = this; From 4fb16c716506c1d023d42336739dff1e8566b127 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 13 Dec 2024 14:41:37 -0300 Subject: [PATCH 61/70] fix when playfromQueue --- packages/player/ios/Classes/SMPlayer.swift | 2 ++ packages/player/ios/Classes/SMPlayerListeners.swift | 8 +++++--- packages/player/lib/src/player.dart | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 9c64e744..eb4ae17e 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -335,6 +335,7 @@ public class SMPlayer : NSObject { func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false) { if (loadOnly) { seekToLoadOnly = true + listeners?.mediaChange?.invalidate() } listeners?.removeItemObservers() distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position, completionHandler: { @@ -349,6 +350,7 @@ public class SMPlayer : NSObject { }else{ play() } + listeners?.addMediaChangeObserver() } func setupNowPlayingInfoCenter(){ diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index f4fcbf1c..e399816b 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -14,7 +14,7 @@ public class SMPlayerListeners : NSObject { addPlayerObservers() } - private var mediaChange: NSKeyValueObservation? + var mediaChange: NSKeyValueObservation? private var statusChange: NSKeyValueObservation? private var loading: NSKeyValueObservation? private var loaded: NSKeyValueObservation? @@ -46,8 +46,7 @@ public class SMPlayerListeners : NSObject { } - - func addPlayerObservers() { + func addMediaChangeObserver(){ mediaChange = smPlayer.observe(\.currentItem, options: [.new, .old]) { [weak self] (player, change) in guard let self = self else { return } let oldItemExists = change.oldValue != nil @@ -58,6 +57,9 @@ public class SMPlayerListeners : NSObject { self.addItemsObservers() } } + } + func addPlayerObservers() { + addMediaChangeObserver() let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 127e1def..6ba497a0 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -141,7 +141,7 @@ class Player { bool autoPlay = false, bool saveOnTop = false, bool alreadyAddedToStorage = false, - bool shouldNotifyTransition = false, + bool shouldNotifyTransition = true, }) async { if (!alreadyAddedToStorage) { _queue.addAll(items, saveOnTop: saveOnTop); From 0f471b2db8d6c4ee28f52b33420f92c0cb52941a Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 13 Dec 2024 17:33:07 -0300 Subject: [PATCH 62/70] added cookie from all batches --- packages/player/ios/Classes/SMPlayer.swift | 7 ++++++- packages/player/lib/src/player.dart | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index eb4ae17e..3f439f3b 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -6,6 +6,7 @@ private var playlistItemKey: UInt8 = 0 var currentRepeatmode: AVQueuePlayer.RepeatMode = .REPEAT_MODE_OFF public class SMPlayer : NSObject { var methodChannelManager: MethodChannelManager? + private var cookie: String = "" //Queue handle private var smPlayer: AVQueuePlayer private var historyQueue: [AVPlayerItem] = [] @@ -119,11 +120,14 @@ public class SMPlayer : NSObject { var playerItem: AVPlayerItem? guard let message = MessageBuffer.shared.receive() else { return } self.shouldNotifyTransition = shouldNotifyTransition + if(!cookie.isEmpty){ + self.cookie = cookie + } let isFirstBatch = self.smPlayer.items().count == 0 for media in message { if(media.url!.contains("https")){ guard let url = URL(string: media.url!) else { continue } - let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie]] + let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": self.cookie]] playerItem = AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) }else{ playerItem = AVPlayerItem(asset:AVAsset(url: NSURL(fileURLWithPath: media.url!) as URL)) @@ -137,6 +141,7 @@ public class SMPlayer : NSObject { self.smPlayer.play() self.setNowPlaying() } + print("#ENQUEUE: shouldNotifyTransition: \(shouldNotifyTransition)") if(shouldNotifyTransition){ methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) } diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 6ba497a0..f2889559 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -177,7 +177,8 @@ class Player { 'playerId': playerId, 'shallSendEvents': _shallSendEvents, 'externalplayback': externalPlayback, - 'shouldNotifyTransition': shouldNotifyTransition, + 'shouldNotifyTransition': + batch.length > 1 ? false : shouldNotifyTransition, if (i == 0) ...{ 'cookie': cookie, }, From 8fab549a8a69fb6c4f5a017f81ec441eeb356c88 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 17 Dec 2024 11:18:13 -0300 Subject: [PATCH 63/70] before cast test --- .../player/MediaButtonEventHandler.kt | 22 ++++++++++++++---- .../br/com/suamusica/player/MediaService.kt | 9 ++++++++ .../player/MediaSessionConnection.kt | 9 +++++++- .../suamusica/player/PlayerChangeNotifier.kt | 2 +- .../br/com/suamusica/player/PlayerPlugin.kt | 23 ++++++++++++------- .../com/suamusica/player/PlayerSingleton.kt | 11 ++++----- packages/player/lib/src/player.dart | 22 ++++-------------- 7 files changed, 60 insertions(+), 38 deletions(-) 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 1fdba275..2a9611f2 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt @@ -40,6 +40,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.SET_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE +import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_IS_PLAYING import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_MEDIA_URI import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures @@ -92,6 +93,7 @@ class MediaButtonEventHandler( add(SessionCommand("ads_playing", Bundle.EMPTY)) add(SessionCommand("onTogglePlayPause", Bundle.EMPTY)) add(SessionCommand(UPDATE_MEDIA_URI, session.token.extras)) + add(SessionCommand(UPDATE_IS_PLAYING, session.token.extras)) }.build() val playerCommands = @@ -122,6 +124,10 @@ class MediaButtonEventHandler( buildIcons() } + if(customCommand.customAction == UPDATE_IS_PLAYING){ + buildIcons() + } + if (customCommand.customAction == "seek") { mediaService.seek(args.getLong("position"), args.getBoolean("playWhenReady")) } @@ -365,25 +371,31 @@ class MediaButtonEventHandler( KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { Log.d("Player", "Player: Key Code : PlayPause") - mediaService.togglePlayPause() + if(session.player.isPlaying){ + PlayerSingleton.pause() + }else{ + PlayerSingleton.play() + } } KeyEvent.KEYCODE_MEDIA_NEXT -> { Log.d("Player", "Player: Key Code : Next") - session.player.seekToNext() +// session.player.seekToNext() + PlayerSingleton.next() } KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { Log.d("Player", "Player: Key Code : Previous") - session.player.seekToPrevious() +// session.player.seekToPrevious() + PlayerSingleton.previous() } KeyEvent.KEYCODE_MEDIA_STOP -> { PlayerSingleton.stop() } } - } else if (intent.hasExtra(PlayerPlugin.FAVORITE)) { - PlayerSingleton.favorite(intent.getBooleanExtra(PlayerPlugin.FAVORITE, false)) + } else if (intent.hasExtra(FAVORITE)) { + PlayerSingleton.favorite(intent.getBooleanExtra(FAVORITE, false)) } return } 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 652d6c25..5d270ba7 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt @@ -609,6 +609,7 @@ class MediaService : MediaSessionService() { reason: @MediaItemTransitionReason Int ) { super.onMediaItemTransition(mediaItem, reason) + Log.d(TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason") if ((player?.mediaItemCount ?: 0) > 0) { playerChangeNotifier?.currentMediaIndex( currentIndex(), @@ -682,6 +683,14 @@ class MediaService : MediaSessionService() { } override fun run() { + val position = player?.currentPosition ?: 0L + val duration = player?.duration ?: 0L + + if (duration > 0 && position >= duration - 800) { + playerChangeNotifier?.notifyStateChange(STATE_ENDED) + Log.d(TAG, "#NATIVE LOGS ==> Notifying COMPLETED print ONLY") + } + notifyPositionChange() if (!shutdownRequest.get()) { 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 a26bb11d..c8822a1c 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt @@ -15,6 +15,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.ID_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.IS_PLAYING_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY import br.com.suamusica.player.PlayerPlugin.Companion.NEW_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD @@ -28,6 +29,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.SET_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE +import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_IS_PLAYING import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_MEDIA_URI import com.google.gson.Gson import java.lang.ref.WeakReference @@ -102,7 +104,7 @@ class MediaSessionConnection( } fun togglePlayPause() { - sendCommand("togglePlayPause", null) + sendCommand("onTogglePlayPause", null) } fun adsPlaying() { @@ -119,6 +121,11 @@ class MediaSessionConnection( bundle.putInt(ID_FAVORITE_ARGUMENT, idFavorite) sendCommand(UPDATE_FAVORITE, bundle) } + fun updatePlayState(isPlaying: Boolean) { + val bundle = Bundle() + bundle.putBoolean(IS_PLAYING_ARGUMENT, isPlaying) + sendCommand(UPDATE_IS_PLAYING, bundle) + } fun updateMediaUri(id:Int,newUri:String?){ val bundle = Bundle() diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index 8aaae546..b05d715d 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -14,7 +14,7 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { STATE_READY -> PlayerState.STATE_READY else -> PlayerState.IDLE } - Log.i("Player", "Notifying Player State change: $playerState | $state") + Log.i("Player", "#NATIVE LOGS ==> Notifying Player State change: $playerState | $state") channelManager.notifyPlayerStateChange("sua-musica-player", playerState) } 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 d59f25fa..1c3ea6fa 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 @@ -50,7 +50,9 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val TOGGLE_SHUFFLE = "toggle_shuffle" const val REPEAT_MODE = "repeat_mode" const val DISABLE_REPEAT_MODE = "disable_repeat_mode" + const val UPDATE_NOTIFICATION = "update_notification" const val UPDATE_FAVORITE = "update_favorite" + const val UPDATE_IS_PLAYING = "update_is_playing" const val UPDATE_MEDIA_URI = "update_media_uri" const val STOP_METHOD = "stop" const val RELEASE_METHOD = "release" @@ -131,11 +133,12 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { ) when (call.method) { ENQUEUE -> { - val batch: Map = call.arguments()!! + val batch: Map = call.arguments() ?: emptyMap() val listMedia: List> = batch["batch"] as List> val autoPlay: Boolean = (batch["autoPlay"] ?: false) as Boolean - val shouldNotifyTransition: Boolean = (batch["shouldNotifyTransition"] ?: false) as Boolean + val shouldNotifyTransition: Boolean = + (batch["shouldNotifyTransition"] ?: false) as Boolean val json = Gson().toJson(listMedia) PlayerSingleton.mediaSessionConnection?.enqueue( json, @@ -183,10 +186,10 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { PlayerSingleton.mediaSessionConnection?.toggleShuffle(positionsList) } - UPDATE_MEDIA_URI ->{ + UPDATE_MEDIA_URI -> { val id = call.argument("id") ?: 0 val uri = call.argument("uri") - PlayerSingleton.mediaSessionConnection?.updateMediaUri(id,uri) + PlayerSingleton.mediaSessionConnection?.updateMediaUri(id, uri) } REPEAT_MODE -> { @@ -201,10 +204,14 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { PlayerSingleton.mediaSessionConnection?.previous() } - UPDATE_FAVORITE -> { - val isFavorite = call.argument(IS_FAVORITE_ARGUMENT) ?: false - val idFavorite = call.argument(ID_FAVORITE_ARGUMENT) ?: 0 - PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite, idFavorite) + UPDATE_NOTIFICATION -> { + val isFavorite = call.argument(IS_FAVORITE_ARGUMENT) + if (isFavorite != null) { + val idFavorite = call.argument(ID_FAVORITE_ARGUMENT) ?: 0 + PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite, idFavorite) + } else { + PlayerSingleton.mediaSessionConnection?.updatePlayState(call.argument(IS_PLAYING_ARGUMENT)) + } } PLAY_FROM_QUEUE_METHOD -> { diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt index 9e72ff98..e0d9e51e 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt @@ -28,11 +28,11 @@ object PlayerSingleton { channel?.invokeMethod("commandCenter.onPlay", emptyMap()) } } -//TODO(Lucas) verificar se pode retirar -// fun togglePlayPause(){ -// mediaSessionConnection?.togglePlayPause() -// channel?.invokeMethod("commandCenter.onTogglePlayPause", emptyMap()) -// } + + fun togglePlayPause(){ + mediaSessionConnection?.togglePlayPause() + channel?.invokeMethod("commandCenter.onTogglePlayPause", emptyMap()) + } // fun adsPlaying(){ // mediaSessionConnection?.adsPlaying() // } @@ -50,7 +50,6 @@ object PlayerSingleton { } fun next() { - Log.d("Player", "#MEDIA3# - commandCenter NEXT") channel?.invokeMethod("commandCenter.onNext", emptyMap()) } diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index f2889559..a628ffb2 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -104,18 +104,8 @@ class Player { return NotOk; } arguments ??= const {}; - // if (_cookies == null || !_cookies!.isValid) { - // _log("Generating Cookies"); - // _cookies = await cookieSigner(); - // } - // String cookie = _cookies!.toHeaders(); - // if (method == "play") { - // _log("Cookie: $cookie"); - // } - final Map args = Map.of(arguments) ..['playerId'] = playerId - // ..['cookie'] = cookie ..['shallSendEvents'] = _shallSendEvents ..['externalplayback'] = externalPlayback; @@ -273,7 +263,9 @@ class Player { Future play({bool shouldPrepare = false}) async { await _invokeMethod( 'play', - {'shouldPrepare': shouldPrepare}, + { + 'shouldPrepare': shouldPrepare, + }, ); return Ok; } @@ -361,16 +353,12 @@ class Player { if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { setRepeatMode("all"); } - return _doNext(); + return _invokeMethod('next'); } else { return null; } } - Future _doNext() async { - return _invokeMethod('next').then((result) => result); - } - Future updateFavorite({ required bool isFavorite, required int id, @@ -577,7 +565,7 @@ class Player { break; case 'commandCenter.onNext': _log("Player : Command Center : Got a next request"); - player.next(); + await player.next(); if (currentMedia != null) { _addUsingPlayer( player, From b3cb5238e6453e1cba6118e4f3fa3bee0442a330 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 17 Dec 2024 14:02:34 -0300 Subject: [PATCH 64/70] disable notification when has ads --- .../player/ios/Classes/PlayerPlugin.swift | 6 +++++ packages/player/ios/Classes/SMPlayer.swift | 23 ++++++++++++++----- packages/player/lib/src/player.dart | 10 ++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index 44ee738d..0c3fa5a4 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -38,6 +38,12 @@ public class PlayerPlugin: NSObject, FlutterPlugin { case "next": smPlayer?.nextTrack(from:"next call") result(NSNumber(value: 1)) + case "disable_notification_commands": + smPlayer?.areNotificationCommandsEnabled = false + result(NSNumber(value: 1)) + case "enable_notification_commands": + smPlayer?.areNotificationCommandsEnabled = true + result(NSNumber(value: 1)) case "previous": smPlayer?.previousTrack() result(NSNumber(value: 1)) diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 3f439f3b..ee51a444 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -20,6 +20,7 @@ public class SMPlayer : NSObject { private var seekToLoadOnly: Bool = false // Transition Control private var shouldNotifyTransition: Bool = false + var areNotificationCommandsEnabled: Bool = true var fullQueue: [AVPlayerItem] { return historyQueue + smPlayer.items() + futureQueue @@ -366,27 +367,37 @@ public class SMPlayer : NSObject { commandCenter.changePlaybackPositionCommand.isEnabled = true commandCenter.pauseCommand.addTarget { [self]event in - smPlayer.pause() + if(areNotificationCommandsEnabled){ + smPlayer.pause() + } return .success } commandCenter.playCommand.addTarget { [self]event in - smPlayer.play() + if(areNotificationCommandsEnabled){ + smPlayer.play() + } return .success } commandCenter.nextTrackCommand.addTarget {[self]event in - nextTrack(from: "commandCenter.nextTrackCommand") + if(areNotificationCommandsEnabled){ + nextTrack(from: "commandCenter.nextTrackCommand") + } return .success } commandCenter.previousTrackCommand.addTarget {[self]event in - previousTrack() + if(areNotificationCommandsEnabled){ + previousTrack() + } return .success } commandCenter.changePlaybackPositionCommand.addTarget{[self]event in - let e = event as? MPChangePlaybackPositionCommandEvent - seekToPosition(position: Int((e?.positionTime ?? 0) * 1000)) + if(areNotificationCommandsEnabled){ + let e = event as? MPChangePlaybackPositionCommandEvent + seekToPosition(position: Int((e?.positionTime ?? 0) * 1000)) + } return .success } } diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index a628ffb2..b71bf7b5 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -270,6 +270,16 @@ class Player { return Ok; } + Future disableNotificatonCommands() async { + await _invokeMethod('disable_notification_commands'); + return Ok; + } + + Future enableNotificatonCommands() async { + await _invokeMethod('enable_notification_commands'); + return Ok; + } + Future playFromQueue( int pos, { Duration? position, From df5a115caddf09b8507a819dc090821cb135cbce Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 17 Dec 2024 18:15:13 -0300 Subject: [PATCH 65/70] fix lastPrevious --- packages/player/ios/Classes/SMPlayer.swift | 14 ++++++- packages/player/lib/src/player.dart | 7 +++- packages/player/lib/src/queue.dart | 48 ++++++---------------- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index ee51a444..7e8ac6c3 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -194,6 +194,7 @@ public class SMPlayer : NSObject { } func nextTrack(from:String) { + smPlayer.pause() print("#print nextTrack \(from)") if let currentItem = smPlayer.currentItem { historyQueue.append(currentItem) @@ -206,11 +207,13 @@ public class SMPlayer : NSObject { seekToPosition(position: 0) setNowPlaying() insertIntoPlayerIfNeeded() + smPlayer.play() printStatus(from:"NEXT") } func previousTrack() { smPlayer.pause() + guard let lastHistoryItem = historyQueue.popLast() else { seekToPosition(position: 0) return @@ -403,8 +406,7 @@ public class SMPlayer : NSObject { } func printStatus(from:String) { - //TODO: adicionar variavel de debug - if(true){ + if(isDebugMode()){ print("QueueActivity #################################################") print("QueueActivity \(from) ") print("QueueActivity Current Index: \(String(describing: currentIndex))") @@ -430,6 +432,14 @@ public class SMPlayer : NSObject { } } + func isDebugMode() -> Bool { + #if DEBUG + return true + #else + return false + #endif + } + //override automatic next @objc func itemDidFinishPlaying(_ notification: Notification) { pause() diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index b71bf7b5..de65001f 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -340,9 +340,14 @@ class Player { } Future previous({bool isFromChromecast = false}) async { + if (_queue.shouldRewind()) { + seek(Duration(milliseconds: 0)); + print("#APP LOGS ==> shouldRewind"); + return Ok; + } + Media? media = _queue.possiblePrevious(); if (isFromChromecast && media != null) { - _queue.lastPrevious = DateTime.now(); return _queue.items.indexOf(media); } if (media == null) { diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index bad14c76..0e4adb61 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -65,7 +65,6 @@ class Queue { var storage = >[]; PreviousPlaylistMusics? previousPlaylistMusics; DateTime? _lastPrevious; - set lastPrevious(DateTime? value) => _lastPrevious = value; Media? get current => _current ?? (index >= 0 && index < storage.length ? storage[index].item : null); @@ -248,49 +247,28 @@ class Queue { return storage.length; } - Media rewind() { - assert(index >= 0 && index < storage.length); - return storage[index].item; - } - - Media previous() { - assert(index >= 0); - final now = DateTime.now(); - if (_lastPrevious == null) { - _lastPrevious = now; - return rewind(); - } else { - final diff = now.difference(_lastPrevious!).inMilliseconds; - print("diff: $diff"); - if (diff < 3000) { - if (index > 0) { - setIndex = index - 1; - } - return storage[index].item; + bool shouldRewind() { + if (index >= 0) { + final now = DateTime.now(); + if (_lastPrevious == null) { + _lastPrevious = now; + return true; } else { + final diff = now.difference(_lastPrevious!).inMilliseconds; _lastPrevious = now; - return rewind(); + return diff > 3000; } } + return false; } Media? possiblePrevious() { if (index >= 0) { - final now = DateTime.now(); - if (_lastPrevious == null) { - return storage[index].item; - } else { - final diff = now.difference(_lastPrevious!).inMilliseconds; - if (diff < 3000) { - var workIndex = index; - if (index > 0) { - --workIndex; - } - return storage[workIndex].item; - } else { - return storage[index].item; - } + var workIndex = index; + if (index > 0) { + --workIndex; } + return storage[workIndex].item; } return storage.isNotEmpty && index >= 0 ? storage[index].item : null; } From b18e5fb6961774309627b78c1d1f206d0ea63b59 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 17 Dec 2024 21:06:40 -0300 Subject: [PATCH 66/70] enable commandCenter buttons --- packages/player/ios/Classes/PlayerPlugin.swift | 1 + packages/player/ios/Classes/SMPlayer.swift | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index 0c3fa5a4..605c89fd 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -43,6 +43,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { result(NSNumber(value: 1)) case "enable_notification_commands": smPlayer?.areNotificationCommandsEnabled = true + smPlayer?.enableCommands() result(NSNumber(value: 1)) case "previous": smPlayer?.previousTrack() diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 7e8ac6c3..183542fd 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -52,7 +52,7 @@ public class SMPlayer : NSObject { if(seekToLoadOnly){ seekToLoadOnly = false methodChannelManager?.currentMediaIndex(index: self.currentIndex) - } + } } } setupNowPlayingInfoCenter() @@ -205,7 +205,6 @@ public class SMPlayer : NSObject { } smPlayer.advanceToNextItem() seekToPosition(position: 0) - setNowPlaying() insertIntoPlayerIfNeeded() smPlayer.play() printStatus(from:"NEXT") @@ -231,7 +230,6 @@ public class SMPlayer : NSObject { smPlayer.insert(currentItem, after: smPlayer.currentItem) seekToPosition(position: 0) - setNowPlaying() insertIntoPlayerIfNeeded() smPlayer.play() printStatus(from:"previousTrack") @@ -362,6 +360,13 @@ public class SMPlayer : NSObject { listeners?.addMediaChangeObserver() } + func enableCommands(){ + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.nextTrackCommand.isEnabled = true; + commandCenter.previousTrackCommand.isEnabled = true; + commandCenter.changePlaybackPositionCommand.isEnabled = true + } + func setupNowPlayingInfoCenter(){ UIApplication.shared.beginReceivingRemoteControlEvents() let commandCenter = MPRemoteCommandCenter.shared() From 86349e40ac734866433c2726c9ff3f6090f48bda Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 18 Dec 2024 00:07:27 -0300 Subject: [PATCH 67/70] remove notification when clear queue --- .../ios/Classes/AudioSessionManager.swift | 17 +++++------------ packages/player/ios/Classes/PlayerPlugin.swift | 4 ++++ packages/player/ios/Classes/SMPlayer.swift | 15 +++++++++++++++ packages/player/lib/src/player.dart | 5 +++++ 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/player/ios/Classes/AudioSessionManager.swift b/packages/player/ios/Classes/AudioSessionManager.swift index 214ca6bc..cf94c393 100644 --- a/packages/player/ios/Classes/AudioSessionManager.swift +++ b/packages/player/ios/Classes/AudioSessionManager.swift @@ -8,18 +8,11 @@ import MediaPlayer return _isActive; } - @objc public static func activeSession() -> Bool { - let audioSession = AVAudioSession.sharedInstance() - do { - if #available(iOS 11, *) { - try audioSession.setCategory(.playback, mode: .default, policy: .longFormAudio) - } else if #available(iOS 10, *) { - let audioSessionCategory: AVAudioSession.CategoryOptions = [.allowAirPlay, .allowBluetoothA2DP] - try audioSession.setCategory(.playback, mode: .default, options: audioSessionCategory) - } else { - // Workaround until https://forums.swift.org/t/using-methods-marked-unavailable-in-swift-4-2/14949 isn't fixed - audioSession.perform(NSSelectorFromString("setCategory:error:"), with: AVAudioSession.Category.playback) - } + @objc public static func activeSession() -> Bool { + let audioSession = AVAudioSession.sharedInstance() + do { + let audioSessionCategory: AVAudioSession.CategoryOptions = [.allowAirPlay, .allowBluetoothA2DP] + try audioSession.setCategory(.playback, mode: .default, options: audioSessionCategory) } catch let error as NSError { print("Player: Failed to set Audio Category \(error.localizedDescription)") return false diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index 605c89fd..c9ea0c29 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -45,6 +45,9 @@ public class PlayerPlugin: NSObject, FlutterPlugin { smPlayer?.areNotificationCommandsEnabled = true smPlayer?.enableCommands() result(NSNumber(value: 1)) + case "remove_notification": + smPlayer?.removeNotification() + result(NSNumber(value: 1)) case "previous": smPlayer?.previousTrack() result(NSNumber(value: 1)) @@ -144,6 +147,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { deinit { print("PlayerPlugin: deinit") + _ = AudioSessionManager.inactivateSession() smPlayer?.clearNowPlayingInfo() smPlayer?.removeAll() } diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 183542fd..7827fa55 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -115,6 +115,7 @@ public class SMPlayer : NSObject { func clearNowPlayingInfo() { MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + removeNotification() } func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String, shouldNotifyTransition: Bool) { @@ -141,11 +142,13 @@ public class SMPlayer : NSObject { if autoPlay && isFirstBatch { self.smPlayer.play() self.setNowPlaying() + self.enableCommands() } print("#ENQUEUE: shouldNotifyTransition: \(shouldNotifyTransition)") if(shouldNotifyTransition){ methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) } + self.enableCommands() } func removeByPosition(indexes: [Int]) { @@ -263,7 +266,19 @@ public class SMPlayer : NSObject { originalQueue.removeAll() shuffledQueue.removeAll() shuffledIndices.removeAll() + } + + func removeNotification(){ + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.nextTrackCommand.isEnabled = false; + commandCenter.previousTrackCommand.isEnabled = false; + commandCenter.changePlaybackPositionCommand.isEnabled = false + commandCenter.playCommand.removeTarget(self) + commandCenter.pauseCommand.removeTarget(self) + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + try? AVAudioSession.sharedInstance().setActive(false) + UIApplication.shared.endReceivingRemoteControlEvents() } func play(){ diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index de65001f..4ec1ede5 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -126,6 +126,11 @@ class Player { return Ok; } + Future removeNotification() async { + await _channel.invokeMethod('remove_notification'); + return Ok; + } + Future enqueueAll( List items, { bool autoPlay = false, From ea6b4db825441f8d6692b9e95c3e3ea8f70523ba Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 18 Dec 2024 09:48:09 -0300 Subject: [PATCH 68/70] added nullable --- .../src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1c3ea6fa..24cb0d5b 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt @@ -210,7 +210,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { val idFavorite = call.argument(ID_FAVORITE_ARGUMENT) ?: 0 PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite, idFavorite) } else { - PlayerSingleton.mediaSessionConnection?.updatePlayState(call.argument(IS_PLAYING_ARGUMENT)) + PlayerSingleton.mediaSessionConnection?.updatePlayState(call.argument(IS_PLAYING_ARGUMENT) ?: false) } } From 3170071fb16b430acc2571afe983f3ea69dfb51d Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 18 Dec 2024 10:37:14 -0300 Subject: [PATCH 69/70] update function name --- packages/player/lib/src/player.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 4ec1ede5..e8738172 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -379,11 +379,11 @@ class Player { } } - Future updateFavorite({ + Future updateNotification({ required bool isFavorite, required int id, }) async { - return _channel.invokeMethod('update_favorite', { + return _channel.invokeMethod('update_notification', { 'isFavorite': isFavorite, 'idFavorite': id, }).then((result) => result); From f60fc9a4336f5ce1409e2c0ea4a9e33632c73d24 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 27 May 2025 16:01:28 -0300 Subject: [PATCH 70/70] remove specific commit reference for file_picker dependency --- packages/player/pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/player/pubspec.yaml b/packages/player/pubspec.yaml index adf86269..5e8994e6 100644 --- a/packages/player/pubspec.yaml +++ b/packages/player/pubspec.yaml @@ -19,7 +19,6 @@ dependencies: file_picker: git: url: https://github.com/SuaMusica/flutter_file_picker.git - ref: 528cd01a317b45305d293c03e855ec7255c3ab59 smaws: git: url: https://github.com/SuaMusica/flutter_plugins.git