diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index 96ffe242..9d5c26f5 100644 --- a/packages/player/android/build.gradle +++ b/packages/player/android/build.gradle @@ -4,7 +4,7 @@ version '1.0.4' buildscript { ext.kotlin_version = '1.9.20' - ext.media3_version = '1.4.1' + ext.media3_version = '1.5.1' repositories { google() mavenCentral() @@ -32,7 +32,7 @@ kapt { } android { - compileSdkVersion 34 + compileSdk = 35 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -49,7 +49,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1' implementation "androidx.media:media:1.7.0" implementation "org.jetbrains.kotlin:kotlin-reflect" //MEDIA3 DEPENDENCIES @@ -60,8 +60,11 @@ dependencies { implementation "androidx.media3:media3-ui:$media3_version" // implementation files('/Users/lucastonussi/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar') - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1' implementation "com.google.code.gson:gson:2.10.1" - +// api 'com.google.android.gms:play-services-cast-framework:22.0.0' + // CHROMECAST DEPENDENCIES + implementation 'androidx.mediarouter:mediarouter:1.7.0' + implementation 'androidx.media3:media3-cast:1.5.1' } diff --git a/packages/player/android/src/main/AndroidManifest.xml b/packages/player/android/src/main/AndroidManifest.xml index 9fd545b9..8827cbfd 100644 --- a/packages/player/android/src/main/AndroidManifest.xml +++ b/packages/player/android/src/main/AndroidManifest.xml @@ -8,6 +8,9 @@ + diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/Cast.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Cast.kt new file mode 100644 index 00000000..ed885041 --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Cast.kt @@ -0,0 +1,329 @@ +package br.com.suamusica.player + +import android.content.Context +import android.util.Log +import androidx.media3.cast.SessionAvailabilityListener +import androidx.media3.common.util.UnstableApi +import androidx.mediarouter.media.MediaControlIntent +import androidx.mediarouter.media.MediaControlIntent.CATEGORY_LIVE_AUDIO +import androidx.mediarouter.media.MediaControlIntent.CATEGORY_LIVE_VIDEO +import androidx.mediarouter.media.MediaControlIntent.CATEGORY_REMOTE_PLAYBACK +import androidx.mediarouter.media.MediaRouteSelector +import androidx.mediarouter.media.MediaRouter +import androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_DISCONNECTED +import androidx.mediarouter.media.RemotePlaybackClient +import com.google.android.gms.cast.* +import com.google.android.gms.cast.framework.CastContext +import com.google.android.gms.cast.framework.CastState +import com.google.android.gms.cast.framework.CastStateListener +import com.google.android.gms.cast.framework.Session +import com.google.android.gms.cast.framework.SessionManager +import com.google.android.gms.cast.framework.SessionManagerListener +import com.google.android.gms.common.api.PendingResult +import com.google.android.gms.common.api.Status + + +@UnstableApi + +class CastManager( + castContext: CastContext, + context: Context, +) : + SessionAvailabilityListener, + CastStateListener, + Cast.Listener(), + SessionManagerListener, + PendingResult.StatusListener { + companion object { + const val TAG = "Chromecast" + } + + private var mediaRouter = MediaRouter.getInstance(context) + var isConnected = false + private var sessionManager: SessionManager? = null + private var mediaRouterCallback: MediaRouter.Callback? = null + private var onConnectCallback: (() -> Unit)? = null + private var onSessionEndedCallback: (() -> Unit)? = null + private var alreadyConnected = false + private var cookie: String = "" + + init { + castContext.addCastStateListener(this) + sessionManager = castContext.sessionManager + + //TODO: pode remover esse callback? + mediaRouterCallback = object : MediaRouter.Callback() { + override fun onRouteAdded(router: MediaRouter, route: MediaRouter.RouteInfo) { + super.onRouteAdded(router, route) + Log.d(TAG, "#NATIVE LOGS CAST ==> Route added: " + route.getName()) + } + + override fun onRouteRemoved(router: MediaRouter, route: MediaRouter.RouteInfo) { + super.onRouteRemoved(router, route) + Log.d(TAG, "#NATIVE LOGS CAST ==> Route removed: " + route.getName()) + } + + override fun onRouteChanged(router: MediaRouter, route: MediaRouter.RouteInfo) { + super.onRouteChanged(router, route) + Log.d(TAG, "#NATIVE LOGS CAST ==> Route changed: " + route.getName()) + } + + override fun onRouteSelected( + router: MediaRouter, + route: MediaRouter.RouteInfo, + reason: Int + ) { + Log.d( + TAG, + "#NATIVE LOGS CAST ==> Route selected: " + route.getName() + ", reason: " + reason + ) + } + + override fun onRouteUnselected( + router: MediaRouter, + route: MediaRouter.RouteInfo, + reason: Int + ) { + Log.d( + TAG, + "#NATIVE LOGS CAST ==> Route unselected: " + route.getName() + ", reason: " + reason + ) + } + } + + val selector: MediaRouteSelector.Builder = MediaRouteSelector.Builder() + .addControlCategory(CATEGORY_REMOTE_PLAYBACK) + //TODO: remover? + mediaRouterCallback?.let { + mediaRouter.addCallback( + selector.build(), it, + MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN + ) + } + } + +// fun discoveryCast(): List> { +// val casts = mutableListOf>() +// if (castContext.castState != CastState.NO_DEVICES_AVAILABLE) { +// mediaRouter.routes.forEach { +// if (it.deviceType == DEVICE_TYPE_TV && it.id.isNotEmpty()) { +// casts.add( +// mapOf( +// "name" to it.name, +// "id" to it.id, +// ) +// ) +// } +// } +// } +// return casts +// } + + fun connectToCast(idCast: String) { + val item = mediaRouter.routes.firstOrNull { + it.id.contains(idCast) + } + if (!isConnected) { + if (item != null) { + mediaRouter.selectRoute(item) + return + } + } else { + mediaRouter.unselect(UNSELECT_REASON_DISCONNECTED) + } + } + + fun disconnect() { + if (isConnected) { + sessionManager?.endCurrentSession(true) + onSessionEndedCallback?.invoke() + isConnected = false + } + } + +// private fun createQueueItem(mediaItem: MediaItem): MediaQueueItem { +// val mediaInfo = createMediaInfo(mediaItem) +// return MediaQueueItem.Builder(mediaInfo).build() +// } +// +// fun queueLoadCast(mediaItems: List) { +// val mediaQueueItems = mediaItems.map { mediaItem -> +// createQueueItem(mediaItem) +// } +// +// val cookieOk = cookie.replace("CloudFront-Policy=", "{\"CloudFront-Policy\": \"") +// .replace(";CloudFront-Key-Pair-Id=", "\", \"CloudFront-Key-Pair-Id\": \"") +// .replace(";CloudFront-Signature=", "\", \"CloudFront-Signature\": \"") + "\"}" +// +// +// val credentials = JSONObject().put("credentials", cookieOk) +// +// +// val request = sessionManager?.currentCastSession?.remoteMediaClient?.queueLoad( +// mediaQueueItems.toTypedArray(), +// player!!.currentMediaItemIndex, +// 1, +// player.currentPosition, +// credentials, +// ) +// +// request?.addStatusListener(this) +// } + +// fun loadMediaOld() { +// val media = player!!.currentMediaItem +// val url = media?.associatedMedia?.coverUrl!! +// +// val musictrackMetaData = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK) +// musictrackMetaData.putString(MediaMetadata.KEY_TITLE, media.associatedMedia?.name!!) +// musictrackMetaData.putString(MediaMetadata.KEY_ARTIST, media.associatedMedia?.author!!) +// musictrackMetaData.putString(MediaMetadata.KEY_ALBUM_TITLE, "albumName") +// musictrackMetaData.putString("images", url) +// +// media.associatedMedia?.coverUrl?.let { +// musictrackMetaData.addImage(WebImage(Uri.parse(it))) +// } +// +// val mediaInfo = +// MediaInfo.Builder(media.associatedMedia?.url!!) +// .setContentUrl(media.associatedMedia?.url!!) +// .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) +// .setMetadata(musictrackMetaData) +// .build() +// +// val cookieOk = cookie.replace("CloudFront-Policy=", "{\"CloudFront-Policy\": \"") +// .replace(";CloudFront-Key-Pair-Id=", "\", \"CloudFront-Key-Pair-Id\": \"") +// .replace(";CloudFront-Signature=", "\", \"CloudFront-Signature\": \"") + "\"}" +// +// val options = MediaLoadOptions.Builder() +//// .setPlayPosition(player.currentPosition) +// .setCredentials( +// cookieOk +// ) +// .build() +// +// val request = +// sessionManager?.currentCastSession?.remoteMediaClient?.load(mediaInfo, options) +// request?.addStatusListener(this) +// } + +// val remoteMediaClient: RemoteMediaClient? +// get() = sessionManager?.currentCastSession?.remoteMediaClient + + +// private fun createMediaInfo(mediaItem: MediaItem): MediaInfo { +// val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK).apply { +// putString(MediaMetadata.KEY_TITLE, mediaItem.associatedMedia?.name ?: "Title") +// putString(MediaMetadata.KEY_ARTIST, mediaItem.associatedMedia?.author ?: "Artist") +// putString( +// MediaMetadata.KEY_ALBUM_TITLE, +// mediaItem.associatedMedia?.albumTitle ?: "Album" +// ) +// +// mediaItem.associatedMedia?.coverUrl?.let { +// putString("images", it) +// } +// mediaItem.associatedMedia?.coverUrl?.let { coverUrl -> +// try { +// addImage(WebImage(Uri.parse(coverUrl.trim()))) +// } catch (e: Exception) { +// Log.e(TAG, "Failed to add cover image: ${e.message}") +// } +// } +// } +// +// return MediaInfo.Builder(mediaItem.associatedMedia?.url!!) +// .setContentUrl(mediaItem.associatedMedia?.url!!) +// .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) +// .setMetadata(metadata) +// .build() +// } + + //CAST STATE LISTENER + override fun onCastStateChanged(state: Int) { + Log.d( + TAG, + "#NATIVE LOGS CAST ==> RECEIVER UPDATE AVAILABLE ${CastState.toString(state)}" + ) + + if (alreadyConnected && state == CastState.NOT_CONNECTED) { + alreadyConnected = false + } + + if (!alreadyConnected) { + isConnected = state == CastState.CONNECTED + if (isConnected) { + alreadyConnected = true + onConnectCallback?.invoke() + } + } + } + + //SessionAvailabilityListener + override fun onCastSessionAvailable() { + Log.d(TAG, "#NATIVE LOGS CAST ==>- SessionAvailabilityListener: onCastSessionAvailable") + } + + override fun onCastSessionUnavailable() { + Log.d(TAG, "#NATIVE LOGS CAST ==>- SessionAvailabilityListener: onCastSessionUnavailable") + } + + //PendingResult.StatusListener + override fun onComplete(status: Status) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onComplete $status") + } + + + //SESSION MANAGER LISTENER + override fun onSessionEnded(p0: Session, p1: Int) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionEnded") + } + + override fun onSessionEnding(p0: Session) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionEnding") + } + + override fun onSessionResumeFailed(p0: Session, p1: Int) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResumeFailed") + } + + override fun onSessionResumed(p0: Session, p1: Boolean) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResumed") + } + + override fun onSessionResuming(p0: Session, p1: String) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResuming") + } + + override fun onSessionStartFailed(p0: Session, p1: Int) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionStartFailed $p0, $p1") + } + + override fun onSessionStarted(p0: Session, p1: String) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onCastSessionUnavailable") + onSessionEndedCallback?.invoke() + } + + override fun onSessionStarting(p0: Session) { + Log.d(TAG, "#NATIVE LOGS CAST ==> $p0 onSessionStarting") +// OnePlayerSingleton.toggleCurrentPlayer(true) + } + + override fun onSessionSuspended(p0: Session, p1: Int) { + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionSuspended") + } + +// var MediaItem.associatedMedia: Media? +// get() = mediaItemMediaAssociations[this] +// set(value) { +// mediaItemMediaAssociations[this] = value +// } + + fun setOnConnectCallback(callback: () -> Unit) { + onConnectCallback = callback + } + + fun setOnSessionEndedCallback(callback: () -> Unit) { + onSessionEndedCallback = callback + } +} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/CastOptionsProvider.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/CastOptionsProvider.kt new file mode 100644 index 00000000..eb6e9429 --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/CastOptionsProvider.kt @@ -0,0 +1,45 @@ +package br.com.suamusica.player + +import android.content.Context +import android.util.Log + +import com.google.android.gms.cast.framework.CastOptions +import com.google.android.gms.cast.framework.OptionsProvider +import com.google.android.gms.cast.framework.SessionProvider +import com.google.android.gms.cast.framework.media.CastMediaOptions +import com.google.android.gms.cast.framework.media.MediaIntentReceiver +import com.google.android.gms.cast.framework.media.NotificationOptions + +class CastOptionsProvider : OptionsProvider { + override fun getCastOptions(context: Context): CastOptions { + Log.d("Player","#NATIVE LOGS ==> CAST getCastOptions ") + +// val buttonActions = listOf( +// MediaIntentReceiver.ACTION_SKIP_NEXT, +// MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK, +// MediaIntentReceiver.ACTION_SKIP_PREV, +// MediaIntentReceiver.ACTION_STOP_CASTING +// ) + +// val compatButtonAction = intArrayOf(1, 3) +// val notificationOptions = +// NotificationOptions.Builder() +// .setActions(buttonActions, compatButtonAction) +// .setSkipStepMs(30000) +// .build() + val mediaOptions = CastMediaOptions.Builder() +// .setNotificationOptions(notificationOptions) + .setMediaSessionEnabled(false) + .setNotificationOptions(null) + .build() + return CastOptions.Builder() + .setReceiverApplicationId("A715FF7E") + .setCastMediaOptions(mediaOptions) + .build() + } + + override fun getAdditionalSessionProviders(context: Context): List? { + Log.d("Player","#NATIVE LOGS ==> CAST getCastOptions") + return null + } +} \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt index 2a9611f2..2aea2305 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt @@ -5,6 +5,7 @@ import android.os.Build import android.os.Bundle import android.util.Log import android.view.KeyEvent +import androidx.media3.cast.CastPlayer import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Player.COMMAND_GET_TIMELINE @@ -12,6 +13,10 @@ import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM +import androidx.media3.common.Player.REPEAT_MODE_ALL +import androidx.media3.common.Player.REPEAT_MODE_OFF +import androidx.media3.common.Player.REPEAT_MODE_ONE +import androidx.media3.common.Player.STATE_IDLE import androidx.media3.common.util.UnstableApi import androidx.media3.session.CommandButton import androidx.media3.session.LibraryResult @@ -21,13 +26,13 @@ import androidx.media3.session.R.drawable import androidx.media3.session.SessionCommand import androidx.media3.session.SessionResult import br.com.suamusica.player.PlayerPlugin.Companion.DISABLE_REPEAT_MODE -import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE +import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.FAVORITE import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.ID_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT -import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY +import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.NEW_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST @@ -36,12 +41,14 @@ import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN import br.com.suamusica.player.PlayerPlugin.Companion.REORDER import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.SEEK_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.SET_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_FAVORITE import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_IS_PLAYING import br.com.suamusica.player.PlayerPlugin.Companion.UPDATE_MEDIA_URI +import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -70,7 +77,7 @@ class MediaButtonEventHandler( } add(SessionCommand("notification_favoritar", Bundle.EMPTY)) add(SessionCommand("notification_desfavoritar", Bundle.EMPTY)) - add(SessionCommand("seek", session.token.extras)) + add(SessionCommand(SEEK_METHOD, session.token.extras)) add(SessionCommand("pause", Bundle.EMPTY)) add(SessionCommand("stop", Bundle.EMPTY)) add(SessionCommand("next", Bundle.EMPTY)) @@ -80,7 +87,7 @@ class MediaButtonEventHandler( add(SessionCommand(TOGGLE_SHUFFLE, Bundle.EMPTY)) add(SessionCommand(REPEAT_MODE, Bundle.EMPTY)) add(SessionCommand(DISABLE_REPEAT_MODE, Bundle.EMPTY)) - add(SessionCommand(ENQUEUE, session.token.extras)) + add(SessionCommand(ENQUEUE_METHOD, session.token.extras)) add(SessionCommand(REMOVE_ALL, Bundle.EMPTY)) add(SessionCommand(REORDER, session.token.extras)) add(SessionCommand(REMOVE_IN, session.token.extras)) @@ -94,6 +101,8 @@ class MediaButtonEventHandler( add(SessionCommand("onTogglePlayPause", Bundle.EMPTY)) add(SessionCommand(UPDATE_MEDIA_URI, session.token.extras)) add(SessionCommand(UPDATE_IS_PLAYING, session.token.extras)) + add(SessionCommand("cast", session.token.extras)) + add(SessionCommand("cast_next_media", session.token.extras)) }.build() val playerCommands = @@ -124,21 +133,28 @@ class MediaButtonEventHandler( buildIcons() } - if(customCommand.customAction == UPDATE_IS_PLAYING){ + if (customCommand.customAction == "cast") { + mediaService.castWithCastPlayer(args.getString("cast_id")) + } + + if (customCommand.customAction == UPDATE_IS_PLAYING) { buildIcons() } - if (customCommand.customAction == "seek") { - mediaService.seek(args.getLong("position"), args.getBoolean("playWhenReady")) + if (customCommand.customAction == SEEK_METHOD) { + val position = args.getLong("position") + val playWhenReady = args.getBoolean("playWhenReady") + session.player.seekTo(position) + session.player.playWhenReady = playWhenReady } if (customCommand.customAction == FAVORITE) { - val isFavorite = args.getBoolean(IS_FAVORITE_ARGUMENT) + val isFavorite = args.getBoolean(IS_FAVORITE_ARGUMENT) val mediaItem = session.player.currentMediaItem!! updateFavoriteMetadata( session.player, session.player.currentMediaItemIndex, mediaItem, - isFavorite, + isFavorite, ) buildIcons() } @@ -160,9 +176,15 @@ class MediaButtonEventHandler( mediaService.reorder(oldIndex, newIndex, positionsList) } + if (customCommand.customAction == "onTogglePlayPause") { - mediaService.togglePlayPause() + if (session.player.isPlaying) { + session.player.pause() + } else { + session.player.play() + } } + if (customCommand.customAction == TOGGLE_SHUFFLE) { // val list = args.getSerializable("list",ArrayList>()::class.java) val json = args.getString(POSITIONS_LIST) @@ -172,44 +194,91 @@ class MediaButtonEventHandler( mediaService.toggleShuffle(positionsList) } if (customCommand.customAction == REPEAT_MODE) { - mediaService.repeatMode() + session.player.let { + when (it.repeatMode) { + REPEAT_MODE_OFF -> { + it.repeatMode = REPEAT_MODE_ALL + } + + REPEAT_MODE_ONE -> { + it.repeatMode = REPEAT_MODE_OFF + } + + else -> { + it.repeatMode = REPEAT_MODE_ONE + } + } + } } if (customCommand.customAction == DISABLE_REPEAT_MODE) { mediaService.disableRepeatMode() } if (customCommand.customAction == "stop") { - mediaService.stop() + session.player.stop() } if (customCommand.customAction == "play") { - val shouldPrepare = args.getBoolean("shouldPrepare") - mediaService.play(shouldPrepare) + if (session.player.playbackState == STATE_IDLE) { + session.player.prepare() + } + session.player.play() } + if (customCommand.customAction == SET_REPEAT_MODE) { val mode = args.getString("mode") - mediaService.setRepeatMode(mode ?:"") + val convertedMode = when (mode) { + "off" -> REPEAT_MODE_OFF + "one" -> REPEAT_MODE_ONE + "all" -> REPEAT_MODE_ALL + else -> REPEAT_MODE_OFF + } + if (session.player is CastPlayer) { + playerChangeNotifier?.onRepeatChanged(convertedMode) + }else { + session.player.repeatMode = convertedMode + } } + + if (customCommand.customAction == "cast_next_media") { + val json = args.getString("media") + val gson = GsonBuilder().create() + val mediaListType = object : TypeToken() {}.type + val media: Media = gson.fromJson(json, mediaListType) + session.player.setMediaItem(mediaService.createMediaItem(media)) + } + if (customCommand.customAction == PLAY_FROM_QUEUE_METHOD) { - mediaService.playFromQueue( - args.getInt(POSITION_ARGUMENT), args.getLong(TIME_POSITION_ARGUMENT), - args.getBoolean( - LOAD_ONLY - ), - ) + if (session.player is CastPlayer) { + PlayerSingleton.getMediaFromQueue(args.getInt(POSITION_ARGUMENT)) + } else { + mediaService.playFromQueue( + args.getInt(POSITION_ARGUMENT), args.getLong(TIME_POSITION_ARGUMENT), + args.getBoolean( + LOAD_ONLY_ARGUMENT + ), + ) + } } if (customCommand.customAction == "notification_previous" || customCommand.customAction == "previous") { - if(session.player.hasPreviousMediaItem()){ - session.player.seekToPreviousMediaItem() - }else{ - session.player.seekToPrevious() + if (session.player is CastPlayer) { + PlayerSingleton.getPreviousMedia() + } else { + if (session.player.hasPreviousMediaItem()) { + session.player.seekToPreviousMediaItem() + } else { + session.player.seekToPrevious() + } } - mediaService.shouldNotifyTransition = true } if (customCommand.customAction == "notification_next" || customCommand.customAction == "next") { - mediaService.shouldNotifyTransition = true - session.player.seekToNextMediaItem() + if (session.player is CastPlayer) { + PlayerSingleton.getNextMedia() + } else { + session.player.seekToNextMediaItem() + } } + if (customCommand.customAction == "pause") { - mediaService.pause() + session.player.pause() } if (customCommand.customAction == UPDATE_MEDIA_URI) { @@ -242,14 +311,11 @@ class MediaButtonEventHandler( } } PlayerSingleton.favorite(isFavorite) -// } } if (customCommand.customAction == "ads_playing") { -// mediaService.player?.pause() -// mediaService.adsPlaying() mediaService.removeNotification() } - if (customCommand.customAction == ENQUEUE) { + if (customCommand.customAction == ENQUEUE_METHOD) { val json = args.getString("json") val gson = GsonBuilder().create() val mediaListType = object : TypeToken>() {}.type @@ -258,7 +324,6 @@ class MediaButtonEventHandler( mediaService.enqueue( mediaList, args.getBoolean("autoPlay"), - args.getBoolean("shouldNotifyTransition"), ) } return Futures.immediateFuture( @@ -286,7 +351,7 @@ class MediaButtonEventHandler( fun buildIcons() { val isFavorite = - mediaService.player?.currentMediaItem?.mediaMetadata?.extras?.getBoolean( + mediaService.smPlayer?.currentMediaItem?.mediaMetadata?.extras?.getBoolean( IS_FAVORITE_ARGUMENT ) ?: false @@ -371,9 +436,9 @@ class MediaButtonEventHandler( KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { Log.d("Player", "Player: Key Code : PlayPause") - if(session.player.isPlaying){ + if (session.player.isPlaying) { PlayerSingleton.pause() - }else{ + } else { PlayerSingleton.play() } } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt index 5d270ba7..20a7783a 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt @@ -1,31 +1,20 @@ package br.com.suamusica.player -import android.app.ActivityManager -import android.app.NotificationManager +import PlayerSwitcher import android.app.PendingIntent import android.app.Service import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Handler -import android.support.v4.media.session.PlaybackStateCompat -import androidx.core.app.NotificationManagerCompat +import androidx.media3.cast.CastPlayer +import androidx.media3.cast.DefaultMediaItemConverter +import androidx.media3.cast.MediaItemConverter import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.PlaybackException -import androidx.media3.common.PlaybackParameters -import androidx.media3.common.Player -import androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK -import androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION -import androidx.media3.common.Player.MediaItemTransitionReason -import androidx.media3.common.Player.REPEAT_MODE_ALL import androidx.media3.common.Player.REPEAT_MODE_OFF -import androidx.media3.common.Player.REPEAT_MODE_ONE -import androidx.media3.common.Player.STATE_ENDED -import androidx.media3.common.Timeline import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util @@ -38,25 +27,21 @@ import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder -import androidx.media3.exoplayer.source.preload.BasePreloadManager import androidx.media3.session.CacheBitmapLoader import androidx.media3.session.CommandButton import androidx.media3.session.DefaultMediaNotificationProvider -import androidx.media3.session.LibraryResult -import androidx.media3.session.MediaConstants import androidx.media3.session.MediaController -import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService -import androidx.media3.session.SessionError -import br.com.suamusica.player.PlayerPlugin.Companion.FALLBACK_URL +import br.com.suamusica.player.PlayerPlugin.Companion.FALLBACK_URL_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.cookie import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory +import com.google.android.gms.cast.MediaQueueItem +import com.google.android.gms.cast.framework.CastContext import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -64,12 +49,11 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import org.json.JSONObject import java.io.File import java.util.Collections -import java.util.WeakHashMap -import java.util.concurrent.atomic.AtomicBoolean -const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" + const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 @UnstableApi @@ -77,42 +61,71 @@ class MediaService : MediaSessionService() { private val TAG = "MediaService" private val userAgent = "SuaMusica/player (Linux; Android ${Build.VERSION.SDK_INT}; ${Build.BRAND}/${Build.MODEL})" - - private var isForegroundService = false - lateinit var mediaSession: MediaSession private var mediaController: ListenableFuture? = null + private val uAmpAudioAttributes = AudioAttributes.Builder() + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .setUsage(C.USAGE_MEDIA) + .build() - private val uAmpAudioAttributes = - AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA) - .build() - - var player: ExoPlayer? = null - - private var progressTracker: ProgressTracker? = null + private var playerSwitcher: PlayerSwitcher? = null + private var exoPlayer: ExoPlayer? = null + private var castPlayer: CastPlayer? = null private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader private lateinit var mediaButtonEventHandler: MediaButtonEventHandler private var shuffleOrder: DefaultShuffleOrder? = null - - private var seekToLoadOnly: Boolean = false -// private var enqueueLoadOnly: Boolean = false - private var shuffledIndices = mutableListOf() private var autoPlay: Boolean = true - var shouldNotifyTransition: Boolean = true - private val channel = Channel>(Channel.BUFFERED) private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - private val mediaItemMediaAssociations = WeakHashMap() + + //CAST + private var cast: CastManager? = null + private var castContext: CastContext? = null + + val smPlayer get() = exoPlayer override fun onCreate() { super.onCreate() mediaButtonEventHandler = MediaButtonEventHandler(this) - - - player = ExoPlayer.Builder(this).build().apply { + castContext = CastContext.getSharedInstance(this) + + //TODO: nao cai aqui, e o que resolveu (justAudio) foi o setContentType do uAmpAudioAttributes + + // val mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager + + // val audioFocusListener = OnAudioFocusChangeListener { focusChange -> + // Log.d("onAudioFocusChangeTeste", focusChange.toString()) + // when (focusChange) { + // AudioManager.AUDIOFOCUS_LOSS -> { + // Log.d("onAudioFocusChangeTeste", "AUDIOFOCUS_LOSS") + // pause() + // } + // AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + // Log.d("onAudioFocusChangeTeste", "AUDIOFOCUS_LOSS_TRANSIENT") + // pause() + // } + // AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // Log.d("onAudioFocusChangeTeste", "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK") + // smPlayer?.volume = 0.2f + // } + // AudioManager.AUDIOFOCUS_GAIN -> { + // Log.d("onAudioFocusChangeTeste", "AUDIOFOCUS_GAIN") + // smPlayer?.volume = 1.0f + // play() + // } + // } + // } + + // mAudioManager.requestAudioFocus( + // audioFocusListener, + // AudioManager.STREAM_MUSIC, + // AudioManager.AUDIOFOCUS_GAIN + // ) + + + exoPlayer = ExoPlayer.Builder(this).build().apply { setAudioAttributes(uAmpAudioAttributes, true) - addListener(playerEventListener()) setWakeMode(C.WAKE_MODE_NETWORK) setHandleAudioBecomingNoisy(true) preloadConfiguration = ExoPlayer.PreloadConfiguration( @@ -123,7 +136,7 @@ class MediaService : MediaSessionService() { dataSourceBitmapLoader = DataSourceBitmapLoader(applicationContext) - player?.let { + exoPlayer?.let { mediaSession = MediaSession.Builder(this, it) .setBitmapLoader(CacheBitmapLoader(dataSourceBitmapLoader)) .setCallback(mediaButtonEventHandler) @@ -166,12 +179,53 @@ class MediaService : MediaSessionService() { } }) } + playerSwitcher = PlayerSwitcher(exoPlayer!!, mediaButtonEventHandler) + castContext?.let { + cast = CastManager(it, this) + } } + fun castWithCastPlayer(castId: String?) { + if (cast?.isConnected == true) { + cast?.disconnect() + return + } + val items = smPlayer?.getAllMediaItems() + if (!items.isNullOrEmpty()) { + cast?.connectToCast(castId!!) + cast?.setOnConnectCallback { + val index = smPlayer?.currentMediaItemIndex ?: 0 + val currentPosition: Long = smPlayer?.currentPosition ?: 0 + //TODO: verificar playback error do exoplayer ao conectar castPlayer + castPlayer = CastPlayer(castContext!!, CustomMediaItemConverter()) + mediaSession.player = castPlayer!! + playerSwitcher?.setCurrentPlayer( + castPlayer!!, + castContext?.sessionManager?.currentCastSession?.remoteMediaClient + ) + smPlayer?.setMediaItem(items[index]) + smPlayer?.seekTo(currentPosition) + smPlayer?.prepare() + smPlayer?.play() + } + + cast?.setOnSessionEndedCallback { + val currentPosition = smPlayer?.currentPosition ?: 0L + val index = smPlayer?.currentMediaItemIndex ?: 0 + exoPlayer?.let { + mediaSession.player = it + playerSwitcher?.setCurrentPlayer(it) + smPlayer?.prepare() + smPlayer?.seekTo(index, currentPosition) + } + } + } + } + + //TODO: testar se vai dar o erro de startForeground no caso de audioAd fun removeNotification() { Log.d("Player", "removeNotification") - player?.stop() -// NotificationManagerCompat.from(applicationContext).cancelAll() + smPlayer?.stop() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -199,15 +253,16 @@ class MediaService : MediaSessionService() { ): MediaSession = mediaSession override fun onTaskRemoved(rootIntent: Intent?) { - player?.clearMediaItems() + smPlayer?.clearMediaItems() Log.d(TAG, "onTaskRemoved") - player?.stop() - stopTrackingProgress() + smPlayer?.stop() stopSelf() super.onTaskRemoved(rootIntent) } override fun onDestroy() { + exoPlayer?.release() + exoPlayer = null mediaSession.run { releaseAndPerformAndDisableTracking() player.release() @@ -222,55 +277,59 @@ class MediaService : MediaSessionService() { } private fun releasePossibleLeaks() { - player?.release() + smPlayer?.release() mediaSession.release() mediaController = null } - private fun isServiceRunning(): Boolean { - val manager = getSystemService(ACTIVITY_SERVICE) as ActivityManager - for (service in manager.getRunningServices(Int.MAX_VALUE)) { - if ("br.com.suamusica.player.MediaService" == service.service.className) { - return true - } + class CustomMediaItemConverter : MediaItemConverter { + override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem { + val queueItem = DefaultMediaItemConverter().toMediaQueueItem(mediaItem) + queueItem.Writer().setCustomData(JSONObject().put("credentials", cookie)) + return queueItem } - return false + + override fun toMediaItem(mediaQueueItem: MediaQueueItem): MediaItem { + return DefaultMediaItemConverter().toMediaItem(mediaQueueItem) + } + } fun updateMediaUri(index: Int, uri: String?) { -// if (index != player?.currentMediaItemIndex) { - val media = player?.getMediaItemAt(index) + val media = smPlayer?.getMediaItemAt(index) media?.associatedMedia?.let { - player?.removeMediaItem(index) - player?.addMediaSource( + smPlayer?.removeMediaItem(index) + smPlayer?.addMediaSource( index, prepare( cookie, it, - uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL) ?: "" + uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL_ARGUMENT) ?: "" ) ) -// player?.prepare() } -// } } fun toggleShuffle(positionsList: List>) { - player?.shuffleModeEnabled = !(player?.shuffleModeEnabled ?: false) - player?.shuffleModeEnabled?.let { + smPlayer?.shuffleModeEnabled = !(smPlayer?.shuffleModeEnabled ?: false) + smPlayer?.shuffleModeEnabled?.let { if (it) { - shuffledIndices.clear() + PlayerSingleton.shuffledIndices.clear() for (e in positionsList) { - shuffledIndices.add(e["originalPosition"] ?: 0) + PlayerSingleton.shuffledIndices.add(e["originalPosition"] ?: 0) } shuffleOrder = DefaultShuffleOrder( - shuffledIndices.toIntArray(), + PlayerSingleton.shuffledIndices.toIntArray(), System.currentTimeMillis() ) Log.d( TAG, - "toggleShuffle - shuffleOrder is null: ${shuffleOrder == null} | shuffledIndices: ${shuffledIndices.size} - ${player?.mediaItemCount}" + "toggleShuffle - shuffledIndices: ${PlayerSingleton.shuffledIndices.size}" ) - player!!.setShuffleOrder(shuffleOrder!!) + if(mediaSession.player !is CastPlayer){ + shuffleOrder?.let { shuffleOrder -> + smPlayer?.setShuffleOrder(shuffleOrder) + } + } } playerChangeNotifier?.onShuffleModeEnabled(it) } @@ -295,21 +354,14 @@ class MediaService : MediaSessionService() { fun enqueue( medias: List, autoPlay: Boolean, - shouldNotifyTransition: Boolean, ) { - Log.d( - TAG, - "enqueue: mediaItemCount: ${player?.mediaItemCount} | autoPlay: $autoPlay" - ) this.autoPlay = autoPlay - this.shouldNotifyTransition = shouldNotifyTransition - if (player?.mediaItemCount == 0) { - player?.playWhenReady = autoPlay + if (smPlayer?.mediaItemCount == 0) { + smPlayer?.playWhenReady = autoPlay } -// enqueueLoadOnly = autoPlay - android.util.Log.d( - "#NATIVE LOGS ==>", - "enqueue $autoPlay | mediaItemCount: ${player?.mediaItemCount} | shouldNotifyTransition: $shouldNotifyTransition" + Log.d( + TAG, + "#NATIVE LOGS MEDIA SERVICE ==> enqueue $autoPlay | mediaItemCount: ${smPlayer?.mediaItemCount}" ) addToQueue(medias) } @@ -320,19 +372,22 @@ class MediaService : MediaSessionService() { for (i in medias.indices) { mediaSources.add(prepare(cookie, medias[i], "")) } - player?.addMediaSources(mediaSources) - player?.prepare() -// PlayerSingleton.playerChangeNotifier?.notifyItemTransition("createMediaSource") -// playerChangeNotifier?.currentMediaIndex( -// currentIndex(), -// "createMediaSource", -// ) - } - if(shouldNotifyTransition){ - playerChangeNotifier?.notifyItemTransition("Enqueue - createMediaSource") + smPlayer?.addMediaSources(mediaSources) + smPlayer?.prepare() } } + fun createMediaItem(media: Media, uri: Uri? = null): MediaItem { + val metadata = buildMetaData(media) + return MediaItem.Builder() + .setMediaId(media.id.toString()) + .setUri(uri ?: Uri.parse(media.url)) + .setMediaMetadata(metadata) + .setMimeType("audio/mpeg") + .build() + .also { it.associatedMedia = media } + } + private fun prepare(cookie: String, media: Media, urlToPrepare: String): MediaSource { val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setReadTimeoutMs(15 * 1000) @@ -340,19 +395,17 @@ class MediaService : MediaSessionService() { dataSourceFactory.setUserAgent(userAgent) dataSourceFactory.setAllowCrossProtocolRedirects(true) dataSourceFactory.setDefaultRequestProperties(mapOf("Cookie" to cookie)) - val metadata = buildMetaData(media) + val uri = if (urlToPrepare.isEmpty()) { val url = media.url if (url.startsWith("/")) Uri.fromFile(File(url)) else Uri.parse(url) } else { Uri.parse(urlToPrepare) } - val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata) - .setMediaId(media.id.toString()).build() - mediaItem.associatedMedia = media - @C.ContentType val type = Util.inferContentType(uri) - return when (type) { + val mediaItem = createMediaItem(media, uri) + + return when (@C.ContentType val type = Util.inferContentType(uri)) { C.CONTENT_TYPE_HLS -> { HlsMediaSource.Factory(dataSourceFactory) .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) @@ -382,16 +435,18 @@ class MediaService : MediaSessionService() { newIndex: Int, positionsList: List> ) { - if (player?.shuffleModeEnabled == true) { - val list = shuffledIndices.ifEmpty { - positionsList.map { it["originalPosition"] ?: 0 }.toMutableList() + if (mediaSession.player !is CastPlayer) { + if (smPlayer?.shuffleModeEnabled == true) { + val list = PlayerSingleton.shuffledIndices.ifEmpty { + positionsList.map { it["originalPosition"] ?: 0 }.toMutableList() + } + Collections.swap(list, oldIndex, newIndex) + shuffleOrder = + DefaultShuffleOrder(list.toIntArray(), System.currentTimeMillis()) + smPlayer?.setShuffleOrder(shuffleOrder!!) + } else { + smPlayer?.moveMediaItem(oldIndex, newIndex) } - Collections.swap(list, oldIndex, newIndex) - shuffleOrder = - DefaultShuffleOrder(list.toIntArray(), System.currentTimeMillis()) - player?.setShuffleOrder(shuffleOrder!!) - } else { - player?.moveMediaItem(oldIndex, newIndex) } } @@ -399,45 +454,27 @@ class MediaService : MediaSessionService() { val sortedIndexes = indexes.sortedDescending() if (sortedIndexes.isNotEmpty()) { sortedIndexes.forEach { - player?.removeMediaItem(it) - if (shuffledIndices.isNotEmpty()) { - shuffledIndices.removeAt( - shuffledIndices.indexOf( - player?.currentMediaItemIndex ?: 0 + smPlayer?.removeMediaItem(it) + if (PlayerSingleton.shuffledIndices.isNotEmpty()) { + PlayerSingleton.shuffledIndices.removeAt( + PlayerSingleton.shuffledIndices.indexOf( + smPlayer?.currentMediaItemIndex ?: 0 ) ) } } } - if (player?.shuffleModeEnabled == true) { + if (smPlayer?.shuffleModeEnabled == true) { shuffleOrder = DefaultShuffleOrder( - shuffledIndices.toIntArray(), + PlayerSingleton.shuffledIndices.toIntArray(), System.currentTimeMillis() ) - player?.setShuffleOrder(shuffleOrder!!) + smPlayer?.setShuffleOrder(shuffleOrder!!) } } fun disableRepeatMode() { - player?.repeatMode = REPEAT_MODE_OFF - } - - fun repeatMode() { - player?.let { - when (it.repeatMode) { - REPEAT_MODE_OFF -> { - it.repeatMode = REPEAT_MODE_ALL - } - - REPEAT_MODE_ONE -> { - it.repeatMode = REPEAT_MODE_OFF - } - - else -> { - it.repeatMode = REPEAT_MODE_ONE - } - } - } + smPlayer?.repeatMode = REPEAT_MODE_OFF } private fun buildMetaData(media: Media): MediaMetadata { @@ -445,10 +482,11 @@ class MediaService : MediaSessionService() { val bundle = Bundle() bundle.putBoolean(IS_FAVORITE_ARGUMENT, media.isFavorite ?: false) - bundle.putString(FALLBACK_URL, media.fallbackUrl) + bundle.putString(FALLBACK_URL_ARGUMENT, media.fallbackUrl) metadataBuilder.apply { setAlbumTitle(media.name) setArtist(media.author) + //TODO: verificar cover sem internet e null e empty setArtworkUri(Uri.parse(media.bigCoverUrl)) setArtist(media.author) setTitle(media.name) @@ -459,256 +497,30 @@ class MediaService : MediaSessionService() { return metadata } - fun play(shouldPrepare: Boolean = false) { - performAndEnableTracking { - if (shouldPrepare) { - player?.prepare() - } - player?.play() - } - } - - fun setRepeatMode(mode: String) { - player?.repeatMode = when (mode) { - "off" -> REPEAT_MODE_OFF - "one" -> REPEAT_MODE_ONE - "all" -> REPEAT_MODE_ALL - else -> REPEAT_MODE_OFF - } - } - fun playFromQueue(position: Int, timePosition: Long, loadOnly: Boolean = false) { - player?.playWhenReady = !loadOnly - - if (loadOnly) { - seekToLoadOnly = true - } - - player?.seekTo( - if (player?.shuffleModeEnabled == true) shuffledIndices[position] else position, + smPlayer?.playWhenReady = !loadOnly + PlayerSingleton.shouldNotifyTransition = smPlayer?.playWhenReady ?: false + smPlayer?.seekTo( + if (smPlayer?.shuffleModeEnabled == true) PlayerSingleton.shuffledIndices[position] else position, timePosition, ) + if (!loadOnly) { - player?.prepare() - playerChangeNotifier?.notifyItemTransition("playFromQueue") + smPlayer?.prepare() } } fun removeAll() { - player?.stop() - player?.clearMediaItems() + smPlayer?.stop() + smPlayer?.clearMediaItems() } fun seek(position: Long, playWhenReady: Boolean) { - player?.seekTo(position) - player?.playWhenReady = playWhenReady - } - - fun pause() { - performAndDisableTracking { - player?.pause() - } - } - - fun stop() { - performAndDisableTracking { - player?.stop() - } - } - - fun togglePlayPause() { - if (player?.isPlaying == true) { - pause() - } else { - play() - } + smPlayer?.seekTo(position) + smPlayer?.playWhenReady = playWhenReady } private fun releaseAndPerformAndDisableTracking() { - performAndDisableTracking { - player?.stop() - } - } - - - private fun notifyPositionChange() { - var position = player?.currentPosition ?: 0L - val duration = player?.duration ?: 0L - position = if (position > duration) duration else position - playerChangeNotifier?.notifyPositionChange(position, duration) - } - - private fun startTrackingProgress() { - if (progressTracker != null) { - return - } - this.progressTracker = ProgressTracker(Handler()) - } - - private fun stopTrackingProgress() { - progressTracker?.stopTracking() - progressTracker = null - } - - private fun stopTrackingProgressAndPerformTask(callable: () -> Unit) { - if (progressTracker != null) { - progressTracker!!.stopTracking(callable) - } else { - callable() - } - progressTracker = null - } - - private fun performAndEnableTracking(callable: () -> Unit) { - callable() - startTrackingProgress() - } - - private fun performAndDisableTracking(callable: () -> Unit) { - callable() - stopTrackingProgress() - } - - fun currentIndex(): Int { - val position = if (player?.shuffleModeEnabled == true) { - shuffledIndices.indexOf( - player?.currentMediaItemIndex ?: 0 - ) - } else { - player?.currentMediaItemIndex ?: 0 - } - return position - } - - private fun playerEventListener(): Player.Listener { - return object : Player.Listener { - override fun onPositionDiscontinuity( - oldPosition: Player.PositionInfo, - newPosition: Player.PositionInfo, - reason: Int - ) { - if (reason == DISCONTINUITY_REASON_SEEK) { - playerChangeNotifier?.notifySeekEnd() - } - } - - override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - playerChangeNotifier?.notifyPlaying(isPlaying) - if (isPlaying) { -// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PLAYING) - startTrackingProgress() - } else { -// PlayerSingleton.playerChangeNotifier?.notifyStateChange(PlaybackStateCompat.STATE_PAUSED) - stopTrackingProgressAndPerformTask {} - } - } - - override fun onMediaItemTransition( - mediaItem: MediaItem?, - reason: @MediaItemTransitionReason Int - ) { - super.onMediaItemTransition(mediaItem, reason) - Log.d(TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason") - if ((player?.mediaItemCount ?: 0) > 0) { - playerChangeNotifier?.currentMediaIndex( - currentIndex(), - "onMediaItemTransition", - ) - } - mediaButtonEventHandler.buildIcons() - if(reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED || !shouldNotifyTransition){ - return - } - playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: ${reason} | shouldNotifyTransition: ${shouldNotifyTransition}") - shouldNotifyTransition = false - } - - var lastState = PlaybackStateCompat.STATE_NONE - 1 - - override fun onPlaybackStateChanged(playbackState: @Player.State Int) { - super.onPlaybackStateChanged(playbackState) - if (lastState != playbackState) { - lastState = playbackState - playerChangeNotifier?.notifyStateChange(playbackState) - } - - if (playbackState == STATE_ENDED) { - stopTrackingProgressAndPerformTask {} - } - Log.d(TAG, "##onPlaybackStateChanged $playbackState") - } - - override fun onPlayerErrorChanged(error: PlaybackException?) { - super.onPlayerErrorChanged(error) - Log.d(TAG, "##onPlayerErrorChanged ${error}") - - } - - override fun onRepeatModeChanged(repeatMode: @Player.RepeatMode Int) { - super.onRepeatModeChanged(repeatMode) - playerChangeNotifier?.onRepeatChanged(repeatMode) - } - - override fun onPlayerError(error: PlaybackException) { - android.util.Log.d( - "#NATIVE LOGS ==>", - "onPlayerError cause ${error.cause.toString()}" - ) - - playerChangeNotifier?.notifyError( - if (error.cause.toString() - .contains("Permission denied") - ) "Permission denied" else error.message - ) - } - - override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { - } - } - } - - var MediaItem.associatedMedia: Media? - get() = mediaItemMediaAssociations[this] - set(value) { - mediaItemMediaAssociations[this] = value - } - - private inner class ProgressTracker(val handler: Handler) : Runnable { - private val shutdownRequest = AtomicBoolean(false) - private var shutdownTask: (() -> Unit)? = null - - init { - handler.post(this) - } - - override fun run() { - val position = player?.currentPosition ?: 0L - val duration = player?.duration ?: 0L - - if (duration > 0 && position >= duration - 800) { - playerChangeNotifier?.notifyStateChange(STATE_ENDED) - Log.d(TAG, "#NATIVE LOGS ==> Notifying COMPLETED print ONLY") - } - - notifyPositionChange() - - if (!shutdownRequest.get()) { - handler.postDelayed(this, 800 /* ms */) - } else { - shutdownTask?.let { - it() - } - } - } - - fun stopTracking() { - shutdownRequest.set(true) - } - - fun stopTracking(callable: () -> Unit) { - shutdownTask = callable - stopTracking() - } + smPlayer?.stop() } } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt index c8822a1c..ddd47a8c 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaSessionConnection.kt @@ -10,13 +10,13 @@ import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import br.com.suamusica.player.PlayerPlugin.Companion.DISABLE_REPEAT_MODE -import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE +import br.com.suamusica.player.PlayerPlugin.Companion.ENQUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.ID_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.IS_PLAYING_ARGUMENT -import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY +import br.com.suamusica.player.PlayerPlugin.Companion.LOAD_ONLY_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.NEW_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.PLAY_FROM_QUEUE_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.POSITIONS_LIST @@ -25,6 +25,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_ALL import br.com.suamusica.player.PlayerPlugin.Companion.REMOVE_IN import br.com.suamusica.player.PlayerPlugin.Companion.REORDER import br.com.suamusica.player.PlayerPlugin.Companion.REPEAT_MODE +import br.com.suamusica.player.PlayerPlugin.Companion.SEEK_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.SET_REPEAT_MODE import br.com.suamusica.player.PlayerPlugin.Companion.TIME_POSITION_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.TOGGLE_SHUFFLE @@ -66,26 +67,35 @@ class MediaSessionConnection( } } - fun enqueue(medias: String, autoPlay: Boolean,shouldNotifyTransition:Boolean) { + fun enqueue(medias: String, autoPlay: Boolean,) { val bundle = Bundle() bundle.putString("json", medias) bundle.putBoolean("autoPlay", autoPlay) - bundle.putBoolean("shouldNotifyTransition", shouldNotifyTransition) - sendCommand(ENQUEUE, bundle) + sendCommand(ENQUEUE_METHOD, bundle) } fun playFromQueue(index: Int, timePosition: Long, loadOnly: Boolean) { val bundle = Bundle() bundle.putInt(POSITION_ARGUMENT, index) bundle.putLong(TIME_POSITION_ARGUMENT, timePosition) - bundle.putBoolean(LOAD_ONLY, loadOnly) + bundle.putBoolean(LOAD_ONLY_ARGUMENT, loadOnly) sendCommand(PLAY_FROM_QUEUE_METHOD, bundle) } - fun play(shouldPrepare: Boolean = false) { + fun play() { + sendCommand("play", null) + } + + fun cast(id: String) { + val bundle = Bundle() + bundle.putString("cast_id", id) + sendCommand("cast", bundle) + } + + fun setCastMedia(media: String) { val bundle = Bundle() - bundle.putBoolean("shouldPrepare", shouldPrepare) - sendCommand("play", bundle) + bundle.putString("media", media) + sendCommand("cast_next_media", bundle) } fun setRepeatMode(mode: String) { @@ -121,16 +131,17 @@ class MediaSessionConnection( bundle.putInt(ID_FAVORITE_ARGUMENT, idFavorite) sendCommand(UPDATE_FAVORITE, bundle) } + fun updatePlayState(isPlaying: Boolean) { val bundle = Bundle() bundle.putBoolean(IS_PLAYING_ARGUMENT, isPlaying) sendCommand(UPDATE_IS_PLAYING, bundle) } - fun updateMediaUri(id:Int,newUri:String?){ + fun updateMediaUri(id: Int, newUri: String?) { val bundle = Bundle() - bundle.putString(NEW_URI_ARGUMENT,newUri) - bundle.putInt(ID_URI_ARGUMENT,id) + bundle.putString(NEW_URI_ARGUMENT, newUri) + bundle.putInt(ID_URI_ARGUMENT, id) sendCommand(UPDATE_MEDIA_URI, bundle) } @@ -183,7 +194,7 @@ class MediaSessionConnection( val bundle = Bundle() bundle.putLong("position", position) bundle.putBoolean("playWhenReady", playWhenReady) - sendCommand("seek", bundle) + sendCommand(SEEK_METHOD, bundle) } fun release() { diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt index cdda53c1..0945ed87 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MethodChannelManagerArgsBuilder.kt @@ -1,7 +1,5 @@ package br.com.suamusica.player -import com.google.gson.Gson - class MethodChannelManagerArgsBuilder { private val args = mutableMapOf() diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PackageValidator.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PackageValidator.kt index c9b8fa7f..781815a1 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PackageValidator.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PackageValidator.kt @@ -13,7 +13,7 @@ import android.util.Base64 import android.util.Log import androidx.annotation.XmlRes import androidx.media.MediaBrowserServiceCompat -import io.flutter.BuildConfig +import com.google.firebase.encoders.json.BuildConfig import org.xmlpull.v1.XmlPullParserException import java.io.IOException import java.security.MessageDigest @@ -153,20 +153,20 @@ class PackageValidator(context: Context, @XmlRes xmlResId: Int) { private fun buildCallerInfo(callingPackage: String): CallerPackageInfo? { val packageInfo = getPackageInfo(callingPackage) ?: return null - val appName = packageInfo.applicationInfo.loadLabel(packageManager).toString() - val uid = packageInfo.applicationInfo.uid + val appName = packageInfo.applicationInfo?.loadLabel(packageManager).toString() + val uid = packageInfo.applicationInfo?.uid val signature = getSignature(packageInfo) val requestedPermissions = packageInfo.requestedPermissions val permissionFlags = packageInfo.requestedPermissionsFlags val activePermissions = mutableSetOf() requestedPermissions?.forEachIndexed { index, permission -> - if (permissionFlags[index] and PackageInfo.REQUESTED_PERMISSION_GRANTED != 0) { + if (permissionFlags!![index] and PackageInfo.REQUESTED_PERMISSION_GRANTED != 0) { activePermissions += permission } } - return CallerPackageInfo(appName, callingPackage, uid, signature, activePermissions.toSet()) + return CallerPackageInfo(appName, callingPackage, uid!!, signature, activePermissions.toSet()) } /** @@ -193,10 +193,10 @@ class PackageValidator(context: Context, @XmlRes xmlResId: Int) { private fun getSignature(packageInfo: PackageInfo): String? { // Security best practices dictate that an app should be signed with exactly one (1) // signature. Because of this, if there are multiple signatures, reject it. - if (packageInfo.signatures == null || packageInfo.signatures.size != 1) { + if (packageInfo.signatures == null || packageInfo.signatures!!.size != 1) { return null } else { - val certificate = packageInfo.signatures[0].toByteArray() + val certificate = packageInfo.signatures!![0].toByteArray() return getSignatureSha256(certificate) } } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index b05d715d..16c302a4 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -1,20 +1,22 @@ package br.com.suamusica.player - -import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import androidx.media3.common.Player -import androidx.media3.common.Player.* +import androidx.media3.common.Player.STATE_IDLE +import androidx.media3.common.Player.STATE_BUFFERING +import androidx.media3.common.Player.STATE_ENDED +import androidx.media3.common.Player.STATE_READY + class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { - fun notifyStateChange(state: @State Int) { + fun notifyStateChange(state: @Player.State Int) { val playerState = when (state) { - STATE_IDLE, STATE_READY -> PlayerState.IDLE + STATE_IDLE -> PlayerState.IDLE STATE_BUFFERING -> PlayerState.BUFFERING STATE_ENDED -> PlayerState.COMPLETED STATE_READY -> PlayerState.STATE_READY else -> PlayerState.IDLE } - Log.i("Player", "#NATIVE LOGS ==> Notifying Player State change: $playerState | $state") + Log.i("Player", "#NATIVE LOGS Notify ==> Notifying Player State change: $playerState | $state") channelManager.notifyPlayerStateChange("sua-musica-player", playerState) } @@ -23,7 +25,7 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { } fun notifyError(message: String? = null){ - Log.i("Player", "Notifying Error: $message") + Log.i("Player", "#NATIVE LOGS Notify ==> Notifying Error: $message") channelManager.notifyError("sua-musica-player", PlayerState.ERROR, message) } @@ -32,32 +34,23 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { channelManager.notifyPlayerStateChange("sua-musica-player", PlayerState.SEEK_END) } - fun notifyNext() { - Log.i("Player", "Notifying Player Next") - channelManager.notifyNext("sua-musica-player") - } - fun notifyPrevious() { - Log.i("Player", "Notifying Player Previous") - channelManager.notifyPrevious("sua-musica-player") - } fun notifyItemTransition(from:String) { - Log.i("Player", "#NATIVE LOGS ==> notifyItemTransition | FROM: $from") + Log.i("Player", "#NATIVE LOGS Notify ==> notifyItemTransition | FROM: $from") channelManager.notifyItemTransition("sua-musica-player") } fun currentMediaIndex(currentMediaIndex: Int, from: String) { - Log.i("Player", "#NATIVE LOGS ==> currentMediaIndex | FROM: $from | $currentMediaIndex") + Log.i("Player", "#NATIVE LOGS Notify ==> currentMediaIndex | FROM: $from | $currentMediaIndex") channelManager.currentMediaIndex("sua-musica-player", currentMediaIndex) } fun notifyPositionChange(position: Long, duration: Long) { - Log.i("Player", "Notifying Player Position change: position: $position duration: $duration") channelManager.notifyPositionChange("sua-musica-player", position, duration) } fun onRepeatChanged(repeatMode: Int) { - Log.i("Player", "Notifying Player onRepeatChanged: $repeatMode") + Log.i("Player", "#NATIVE LOGS Notify ==> onRepeatChanged: $repeatMode") channelManager.onRepeatChanged("sua-musica-player", repeatMode) } fun onShuffleModeEnabled(shuffleModeEnabled: Boolean) { - Log.i("Player", "Notifying Player onRepeatChanged: $shuffleModeEnabled") + Log.i("Player", "#NATIVE LOGS Notify ==> onRepeatChanged: $shuffleModeEnabled") channelManager.onShuffleModeEnabled("sua-musica-player", shuffleModeEnabled) } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerExtensions.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerExtensions.kt new file mode 100644 index 00000000..ce97c5e7 --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerExtensions.kt @@ -0,0 +1,22 @@ +package br.com.suamusica.player + +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import java.util.WeakHashMap + +val mediaItemMediaAssociations = WeakHashMap() + + +fun Player.getAllMediaItems(): List { + val items = mutableListOf() + for (i in 0 until mediaItemCount) { + getMediaItemAt(i).let { items.add(it) } + } + return items +} + +var MediaItem.associatedMedia: Media? + get() = mediaItemMediaAssociations[this] + set(value) { + mediaItemMediaAssociations[this] = value + } \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt index 24cb0d5b..3a320040 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt @@ -1,5 +1,6 @@ package br.com.suamusica.player +import android.content.pm.ApplicationInfo import android.util.Log import com.google.gson.Gson import com.google.gson.reflect.TypeToken @@ -14,15 +15,13 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { companion object { + private const val CHANNEL = "suamusica.com.br/player" // Argument names - const val NAME_ARGUMENT = "name" - const val AUTHOR_ARGUMENT = "author" - const val URL_ARGUMENT = "url" - const val COVER_URL_ARGUMENT = "coverUrl" - const val BIG_COVER_URL_ARGUMENT = "bigCoverUrl" const val IS_PLAYING_ARGUMENT = "isPlaying" + const val SHOULD_NOTIFY_TRANSITION_ARGUMENT = "shouldNotifyTransition" + const val PLAY_WHEN_READY_ARGUMENT = "playWhenReady" const val IS_FAVORITE_ARGUMENT = "isFavorite" - const val FALLBACK_URL = "fallbackURL" + const val FALLBACK_URL_ARGUMENT = "fallbackURL" const val ID_FAVORITE_ARGUMENT = "idFavorite" const val NEW_URI_ARGUMENT = "newUri" const val ID_URI_ARGUMENT = "idUri" @@ -30,15 +29,15 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val TIME_POSITION_ARGUMENT = "timePosition" const val INDEXES_TO_REMOVE = "indexesToDelete" const val POSITIONS_LIST = "positionsList" - const val LOAD_ONLY = "loadOnly" + const val LOAD_ONLY_ARGUMENT = "loadOnly" const val RELEASE_MODE_ARGUMENT = "releaseMode" - private const val CHANNEL = "suamusica.com.br/player" + const val FAVORITE: String = "favorite" // Method names const val PLAY_METHOD = "play" const val SET_REPEAT_MODE = "set_repeat_mode" - const val ENQUEUE = "enqueue" + const val ENQUEUE_METHOD = "enqueue" const val REMOVE_ALL = "remove_all" const val REMOVE_IN = "remove_in" const val REORDER = "reorder" @@ -50,7 +49,6 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { const val TOGGLE_SHUFFLE = "toggle_shuffle" const val REPEAT_MODE = "repeat_mode" const val DISABLE_REPEAT_MODE = "disable_repeat_mode" - const val UPDATE_NOTIFICATION = "update_notification" const val UPDATE_FAVORITE = "update_favorite" const val UPDATE_IS_PLAYING = "update_is_playing" const val UPDATE_MEDIA_URI = "update_media_uri" @@ -89,10 +87,11 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { override fun onAttachedToActivity(binding: ActivityPluginBinding) { Log.d(TAG, "onAttachedToActivity") -// val isStopped = (binding.activity.applicationInfo.flags and ApplicationInfo.FLAG_STOPPED) == ApplicationInfo.FLAG_STOPPED -// if(!isStopped){ + // val isStopped = + // (binding.activity.applicationInfo.flags and ApplicationInfo.FLAG_STOPPED) == ApplicationInfo.FLAG_STOPPED + // if (!isStopped) { alreadyAttachedToActivity = true -// } + // } } override fun onDetachedFromActivityForConfigChanges() { @@ -118,38 +117,40 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { } private fun handleMethodCall(call: MethodCall, response: MethodChannel.Result) { - if (call.method == ENQUEUE) { + if (call.method == ENQUEUE_METHOD) { val batch: Map = call.arguments()!! cookie = if (batch.containsKey("cookie")) batch["cookie"] as String else cookie - PlayerSingleton.externalPlayback = - if (batch.containsKey("externalplayback")) batch["externalplayback"].toString() == "true" else PlayerSingleton.externalPlayback } else { cookie = call.argument("cookie") ?: cookie - PlayerSingleton.externalPlayback = call.argument("externalplayback") } Log.d( TAG, "method: ${call.method}" ) when (call.method) { - ENQUEUE -> { + ENQUEUE_METHOD -> { val batch: Map = call.arguments() ?: emptyMap() val listMedia: List> = batch["batch"] as List> val autoPlay: Boolean = (batch["autoPlay"] ?: false) as Boolean - val shouldNotifyTransition: Boolean = - (batch["shouldNotifyTransition"] ?: false) as Boolean val json = Gson().toJson(listMedia) PlayerSingleton.mediaSessionConnection?.enqueue( json, autoPlay, - shouldNotifyTransition, ) } - + "cast" -> { + val id = call.argument("castId") ?: "" + PlayerSingleton.mediaSessionConnection?.cast(id) + } + "cast_next_media" -> { + val media: Map = call.arguments() ?: emptyMap() + val json = Gson().toJson(media) + Log.d(TAG,"NEXT MEDIA: $json") + PlayerSingleton.mediaSessionConnection?.setCastMedia(json) + } PLAY_METHOD -> { - val shouldPrepare = call.argument("shouldPrepare") ?: false - PlayerSingleton.mediaSessionConnection?.play(shouldPrepare) + PlayerSingleton.mediaSessionConnection?.play() } SET_REPEAT_MODE -> { @@ -204,20 +205,18 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { PlayerSingleton.mediaSessionConnection?.previous() } - UPDATE_NOTIFICATION -> { + UPDATE_FAVORITE -> { val isFavorite = call.argument(IS_FAVORITE_ARGUMENT) if (isFavorite != null) { val idFavorite = call.argument(ID_FAVORITE_ARGUMENT) ?: 0 PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite, idFavorite) - } else { - PlayerSingleton.mediaSessionConnection?.updatePlayState(call.argument(IS_PLAYING_ARGUMENT) ?: false) } } PLAY_FROM_QUEUE_METHOD -> { val position = call.argument(POSITION_ARGUMENT) ?: 0 val timePosition = call.argument(TIME_POSITION_ARGUMENT) ?: 0 - val loadOnly = call.argument(LOAD_ONLY) ?: false + val loadOnly = call.argument(LOAD_ONLY_ARGUMENT) ?: false PlayerSingleton.mediaSessionConnection?.playFromQueue( position, timePosition.toLong(), @@ -247,7 +246,8 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { SEEK_METHOD -> { val position = call.argument(POSITION_ARGUMENT)!! - PlayerSingleton.mediaSessionConnection?.seek(position, true) + val playWhenReady = call.argument(PLAY_WHEN_READY_ARGUMENT)!! + PlayerSingleton.mediaSessionConnection?.seek(position, playWhenReady) } REMOVE_NOTIFICATION_METHOD -> { diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt index e0d9e51e..92e27d6f 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSingleton.kt @@ -7,10 +7,14 @@ import io.flutter.plugin.common.MethodChannel object PlayerSingleton { var channel: MethodChannel? = null var mediaSessionConnection: MediaSessionConnection? = null - var externalPlayback: Boolean? = false private const val TAG = "Player" var playerChangeNotifier: PlayerChangeNotifier? = null + var shouldNotifyTransition: Boolean = false + + + var shuffledIndices = mutableListOf() + fun setChannel(c: MethodChannel, context: Context) { channel = c playerChangeNotifier = PlayerChangeNotifier(MethodChannelManager(c)) @@ -21,28 +25,19 @@ object PlayerSingleton { } fun play() { - if (externalPlayback!!) { - channel?.invokeMethod("externalPlayback.play", emptyMap()) - } else { + mediaSessionConnection?.play() channel?.invokeMethod("commandCenter.onPlay", emptyMap()) - } } fun togglePlayPause(){ mediaSessionConnection?.togglePlayPause() channel?.invokeMethod("commandCenter.onTogglePlayPause", emptyMap()) } -// fun adsPlaying(){ -// mediaSessionConnection?.adsPlaying() -// } + fun pause() { - if (externalPlayback!!) { - channel?.invokeMethod("externalPlayback.pause", emptyMap()) - } else { mediaSessionConnection?.pause() channel?.invokeMethod("commandCenter.onPause", emptyMap()) - } } fun previous() { @@ -53,6 +48,23 @@ object PlayerSingleton { channel?.invokeMethod("commandCenter.onNext", emptyMap()) } + fun getNextMedia() { + channel?.invokeMethod("cast.nextMedia", emptyMap()) + Log.d(TAG, "#NATIVE LOGS Notify ==> getNextMedia") + } + + fun getPreviousMedia() { + channel?.invokeMethod("cast.previousMedia", emptyMap()) + Log.d(TAG, "#NATIVE LOGS Notify ==> getPreviousMedia") + } + + fun getMediaFromQueue(index: Int) { + val args = mutableMapOf() + args["index"] = index + channel?.invokeMethod("cast.mediaFromQueue", args) + Log.d(TAG, "#NATIVE LOGS Notify ==> getMediaFromQueue | $index") + } + fun stop() { mediaSessionConnection?.stop() } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt index 631d10f6..d1067642 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerState.kt @@ -12,4 +12,5 @@ enum class PlayerState { BUFFER_EMPTY, ITEM_TRANSITION, STATE_READY, + STATE_ENDED, } \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSwitcher.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSwitcher.kt new file mode 100644 index 00000000..a608ab8d --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSwitcher.kt @@ -0,0 +1,360 @@ +import android.R +import android.content.Context +import android.os.Handler +import android.support.v4.media.session.PlaybackStateCompat +import androidx.core.content.res.ResourcesCompat +import androidx.media3.common.C +import androidx.media3.common.ForwardingPlayer +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.ShuffleOrder +import br.com.suamusica.player.getAllMediaItems +import android.util.Log +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player.STATE_ENDED +import br.com.suamusica.player.MediaButtonEventHandler +import br.com.suamusica.player.PlayerSingleton +import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier +import java.util.concurrent.atomic.AtomicBoolean +import android.os.Looper +import androidx.media3.cast.CastPlayer +import com.google.android.gms.cast.framework.media.RemoteMediaClient + + +@UnstableApi +class PlayerSwitcher( + private var currentPlayer: Player, + private var mediaButtonEventHandler: MediaButtonEventHandler, +) : ForwardingPlayer(currentPlayer) { + private var playerEventListener: Player.Listener? = null + private val TAG = "PlayerSwitcher" + private var progressTracker: ProgressTracker? = null + var remoteMediaClient: RemoteMediaClient? = null + private var playerState: PlayerState? = null + + companion object { + private const val ERROR_PERMISSION_DENIED = "Permission denied" + private const val ERROR_UNKNOWN = "Unknown error occurred" + private const val ERROR_PLAYER_NOT_READY = "Player not ready" + private const val ERROR_RESTORE_STATE = "Restore state error" + private const val ERROR_STOP_AND_CLEAR_PLAYER = "Stop and clear player error" + private const val ERROR_SET_CURRENT_PLAYER = "Set current player error" + } + + var oldPlayer: Player? = null + + init { + try { + playerEventListener?.let { currentPlayer.removeListener(it) } + setupPlayerListener() + } catch (e: Exception) { + Log.e(TAG, "Error initializing PlayerSwitcher", e) + throw IllegalStateException(ERROR_PLAYER_NOT_READY, e) + } + } + + fun setCurrentPlayer(newPlayer: Player, remoteMediaClient: RemoteMediaClient? = null) { + try { + if (this.currentPlayer === newPlayer) { + return + } + oldPlayer = currentPlayer + this.remoteMediaClient = remoteMediaClient + playerState = savePlayerState() + playerEventListener?.let { currentPlayer.removeListener(it) } + stopAndClearCurrentPlayer() + this.currentPlayer = newPlayer + if (currentPlayer is CastPlayer) { + restorePlayerState(playerState!!) + } + setupPlayerListener() + } catch (e: Exception) { + Log.e(TAG, "Error setting current player", e) + playerChangeNotifier?.notifyError(ERROR_SET_CURRENT_PLAYER) + } + } + + private data class PlayerState( + val playbackPositionMs: Long = C.TIME_UNSET, + val currentItemIndex: Int = C.INDEX_UNSET, + val playWhenReady: Boolean = false, + val mediaItems: List = emptyList(), + ) + + private fun savePlayerState(): PlayerState { + return PlayerState( + playbackPositionMs = if (currentPlayer.playbackState != STATE_ENDED) currentPlayer.currentPosition else C.TIME_UNSET, + currentItemIndex = currentPlayer.currentMediaItemIndex, + playWhenReady = currentPlayer.playWhenReady, + mediaItems = currentPlayer.getAllMediaItems() + ) + } + + private fun stopAndClearCurrentPlayer() { + try { + currentPlayer.stop() + if (currentPlayer is CastPlayer) { + currentPlayer.clearMediaItems() + } + } catch (e: Exception) { + Log.e(TAG, "Error stopping and clearing current player", e) + playerChangeNotifier?.notifyError(ERROR_STOP_AND_CLEAR_PLAYER) + } + } + + private fun restorePlayerState(state: PlayerState) { + try { + currentPlayer.setMediaItems( + state.mediaItems, + state.currentItemIndex, + state.playbackPositionMs + ) + currentPlayer.playWhenReady = state.playWhenReady + currentPlayer.prepare() + } catch (e: Exception) { + Log.e(TAG, "Error restoring player state", e) + playerChangeNotifier?.notifyError(ERROR_RESTORE_STATE) + } + } + + private fun setupPlayerListener() { + playerEventListener = object : Player.Listener { + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + if (reason == DISCONTINUITY_REASON_SEEK) { + playerChangeNotifier?.notifySeekEnd() + } + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + if (lastState != STATE_BUFFERING) { + playerChangeNotifier?.notifyPlaying(isPlaying) + } + if (isPlaying) { + startTrackingProgress() + } else { + stopTrackingProgress() + } + } + + override fun onMediaItemTransition( + mediaItem: MediaItem?, + reason: @Player.MediaItemTransitionReason Int + ) { + super.onMediaItemTransition(mediaItem, reason) +// oldTransition(mediaItem, reason) + newTransition(reason) + } + + fun newTransition( + reason: @Player.MediaItemTransitionReason Int + ) { + val shouldNotify = + reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == MEDIA_ITEM_TRANSITION_REASON_AUTO + + Log.d( + TAG, + "#NATIVE LOGS ==> onMediaItemTransition reason: $reason | shouldNotNotify: $shouldNotify | shouldNotifyTransition ${PlayerSingleton.shouldNotifyTransition}" + ) + + if (currentPlayer is CastPlayer && reason == MEDIA_ITEM_TRANSITION_REASON_AUTO) { + PlayerSingleton.getNextMedia() + } + + playerChangeNotifier?.currentMediaIndex( + currentIndex(), + "onMediaItemTransition", + ) + + //We not notify when playFromQueue is loadOnly + if (!PlayerSingleton.shouldNotifyTransition) { + return + } + + + mediaButtonEventHandler.buildIcons() + + playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: $reason | shouldNotifyTransition: ${PlayerSingleton.shouldNotifyTransition}") + + PlayerSingleton.shouldNotifyTransition = true + } + + var lastState = PlaybackStateCompat.STATE_NONE - 1 + + override fun onPlaybackStateChanged(playbackState: @Player.State Int) { + super.onPlaybackStateChanged(playbackState) + if (lastState != playbackState) { + lastState = playbackState + playerChangeNotifier?.notifyStateChange(playbackState) + } + + if (playbackState == STATE_ENDED) { + stopTrackingProgressAndPerformTask {} + } + + if (playbackState == STATE_READY && currentPlayer is CastPlayer) { + currentPlayer.repeatMode = REPEAT_MODE_ALL + playerChangeNotifier?.onRepeatChanged(currentPlayer.repeatMode) + } + + Log.d(TAG, "##onPlaybackStateChanged $playbackState") + } + + override fun onPlayerErrorChanged(error: PlaybackException?) { + super.onPlayerErrorChanged(error) + Log.d(TAG, "##onPlayerErrorChanged ${error}") + + } + + override fun onPlayerError(error: PlaybackException) { + Log.d( + "#NATIVE LOGS ==>", + "onPlayerError cause ${error.cause.toString()}" + ) + + playerChangeNotifier?.notifyError( + if (error.cause.toString() + .contains("Permission denied") + ) "Permission denied" else error.message + ) + } + + override fun onRepeatModeChanged(repeatMode: @Player.RepeatMode Int) { + super.onRepeatModeChanged(repeatMode) + playerChangeNotifier?.onRepeatChanged(repeatMode) + } + } + playerEventListener?.let { currentPlayer.addListener(it) } + } + + private fun startTrackingProgress() { + progressTracker?.stopTracking() + progressTracker = ProgressTracker(Handler(Looper.getMainLooper()), this).apply { + setOnPositionChangeListener { position, duration -> + notifyPositionChange() + } + startTracking() + } + } + + fun currentIndex(): Int { + if ((currentPlayer.mediaItemCount) > 0) { + val position = if (currentPlayer.shuffleModeEnabled) { + PlayerSingleton.shuffledIndices.indexOf( + currentPlayer.currentMediaItemIndex + ) + } else { + currentPlayer.currentMediaItemIndex + } + return position + } + return -1 + } + + private fun stopTrackingProgress() { + progressTracker?.stopTracking() + progressTracker = null + } + + + fun setShuffleOrder(shuffleOrder: ShuffleOrder) { + if (currentPlayer is ExoPlayer) { + (currentPlayer as ExoPlayer).setShuffleOrder(shuffleOrder) + } + } + + fun addMediaSource(index: Int, mediaSource: MediaSource) { + if (currentPlayer is ExoPlayer) { + (currentPlayer as ExoPlayer).addMediaSource(index, mediaSource) + } + } + + fun addMediaSources(mediaSources: MutableList) { + if (currentPlayer is ExoPlayer) { + (currentPlayer as ExoPlayer).addMediaSources(mediaSources) + } + } + + + override fun getWrappedPlayer(): Player = currentPlayer + + private fun stopTrackingProgressAndPerformTask(callable: () -> Unit) { + progressTracker?.stopTracking { + callable() + } + progressTracker = null + } + + private fun notifyPositionChange() { + val position = currentPlayer.currentPosition.coerceAtMost(currentPlayer.duration ?: 0L) + val duration = currentPlayer.duration + playerChangeNotifier?.notifyPositionChange(position, duration) + } +} + + +@UnstableApi +class ProgressTracker( + private val handler: Handler, + private val player: PlayerSwitcher, + private val updateIntervalMs: Long = 500 // Intervalo configurável +) : Runnable { + private val TAG = "ProgressTracker" + private val isTracking = AtomicBoolean(false) + private var shutdownTask: (() -> Unit)? = null + private var onPositionChange: ((Long, Long) -> Unit)? = null + + fun startTracking() { + if (isTracking.compareAndSet(false, true)) { + handler.post(this) + } + } + + override fun run() { + if (!isTracking.get()) return + + try { + val position = player.wrappedPlayer.currentPosition + val duration = player.wrappedPlayer.duration + + // Verifica se está próximo do fim + if (duration > 0 && position >= duration - END_THRESHOLD_MS) { + playerChangeNotifier?.notifyStateChange(STATE_ENDED) + Log.d(TAG, "Track completing: position=$position, duration=$duration") + } + + // Notifica mudança de posição + onPositionChange?.invoke(position, duration) + + // Agenda próxima atualização + if (isTracking.get()) { + handler.postDelayed(this, updateIntervalMs) + } + } catch (e: Exception) { + Log.e(TAG, "Error during progress tracking", e) + stopTracking() + } + } + + fun stopTracking(onStopped: (() -> Unit)? = null) { + if (isTracking.compareAndSet(true, false)) { + handler.removeCallbacks(this) + onStopped?.invoke() + } + } + + fun setOnPositionChangeListener(listener: ((Long, Long) -> Unit)?) { + onPositionChange = listener + } + + companion object { + private const val END_THRESHOLD_MS = 800L + } +} \ No newline at end of file diff --git a/packages/player/example/.flutter-plugins-dependencies b/packages/player/example/.flutter-plugins-dependencies index ca9c0226..4a056563 100644 --- a/packages/player/example/.flutter-plugins-dependencies +++ b/packages/player/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"android":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_android-2.2.10/","native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"macos":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"isar_flutter_libs","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider"]}],"date_created":"2024-09-14 19:45:21.353811","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"mdns_plugin","path":"/Users/lucastonussi/.pub-cache/git/flutter-6ce9697a9c0a20c7224fd75562164bfb91e1e372/mdns_plugin/","native_build":true,"dependencies":[],"dev_dependency":false},{"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":[],"dev_dependency":false},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs","file_picker","mdns_plugin"],"dev_dependency":true}],"android":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.23/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"mdns_plugin","path":"/Users/lucastonussi/.pub-cache/git/flutter-6ce9697a9c0a20c7224fd75562164bfb91e1e372/mdns_plugin/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_android-2.2.10/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs","file_picker","mdns_plugin"],"dev_dependency":true}],"macos":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"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":[],"dev_dependency":false}],"linux":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"isar_flutter_libs","dependencies":[]},{"name":"mdns_plugin","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider","file_picker","mdns_plugin"]}],"date_created":"2025-05-27 10:11:16.511994","version":"3.32.0","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/packages/player/example/android/app/build.gradle b/packages/player/example/android/app/build.gradle index 9d68c793..4becbe9e 100644 --- a/packages/player/example/android/app/build.gradle +++ b/packages/player/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 34 + compileSdkVersion 35 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/packages/player/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/packages/player/example/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/packages/player/example/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/packages/player/example/ios/Flutter/ephemeral/flutter_lldbinit b/packages/player/example/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/packages/player/example/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/packages/player/example/lib/main.dart b/packages/player/example/lib/main.dart index 102d9078..d00817da 100644 --- a/packages/player/example/lib/main.dart +++ b/packages/player/example/lib/main.dart @@ -9,21 +9,11 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Player example app'), - ), - body: Material( - child: SMPlayer(), - ), + home: Material( + child: SMPlayer(), ), ); } diff --git a/packages/player/example/lib/service_discovery.dart b/packages/player/example/lib/service_discovery.dart new file mode 100644 index 00000000..6cc816d6 --- /dev/null +++ b/packages/player/example/lib/service_discovery.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +import 'package:mdns_plugin/mdns_plugin.dart'; + +class ServiceDiscovery { + ServiceDiscovery(Function(MDNSService)? onFound) { + _flutterMdnsPlugin = MDNSPlugin( + DelegateMDNS( + (MDNSService service) { + final fn = toUTF8String(service.txt?['fn']); + if (fn != null) { + service.map['name'] = fn; + } + onFound?.call(service); + }, + ), + ); + } + late MDNSPlugin _flutterMdnsPlugin; + void startDiscovery() => _flutterMdnsPlugin.startDiscovery( + '_googlecast._tcp', + enableUpdating: true, + ); + + void stopDiscovery() => _flutterMdnsPlugin.stopDiscovery(); +} + +String? toUTF8String(List? bytes) => + bytes == null ? null : const Utf8Codec().decode(bytes); + +class DelegateMDNS implements MDNSPluginDelegate { + DelegateMDNS(this.resolved); + final Function(MDNSService)? resolved; + @override + void onDiscoveryStarted() {} + @override + void onDiscoveryStopped() {} + @override + void onServiceUpdated(MDNSService service) {} + @override + void onServiceRemoved(MDNSService service) {} + @override + bool onServiceFound(MDNSService service) => true; + @override + void onServiceResolved(MDNSService service) { + resolved?.call(service); + } +} diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 25328700..00eba612 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -4,10 +4,12 @@ import 'package:smplayer/player.dart'; import 'dart:async'; import 'package:flutter/services.dart'; import 'package:smplayer_example/app_colors.dart'; +import 'package:smplayer_example/service_discovery.dart'; import 'package:smplayer_example/ui_data.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:mdns_plugin/mdns_plugin.dart'; class SMPlayer extends StatefulWidget { SMPlayer({ @@ -39,14 +41,14 @@ var media1 = Media( ); var media3 = Media( - id: 1, + id: 3, albumTitle: "Album unsigned", albumId: 1, name: "Track unsigned", url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", coverUrl: "https://picsum.photos/500/500", bigCoverUrl: "https://picsum.photos/500/500", - author: "Xand Avião", + author: "Unknown", fallbackUrl: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", isLocal: false, isVerified: true, @@ -57,7 +59,7 @@ var media3 = Media( ); var media4 = Media( - id: 1, + id: 4, albumTitle: "É o Grelo", albumId: 1, name: "01 - VIDA LOK4 part.1", @@ -99,10 +101,11 @@ var media2 = Media( class _SMPlayerState extends State { late Player _player; Media? currentMedia; - String mediaLabel = ''; Duration duration = Duration(seconds: 0); Duration position = Duration(seconds: 0); var _shuffled = false; + bool _loading = false; + int _currentIndex = 0; @override void initState() { @@ -120,10 +123,14 @@ class _SMPlayerState extends State { initializeIsar: false, ); player.onEvent.listen((Event event) async { - // print( - // "Event: [${event.type}] [${event.media.author}-${event.media.name}] [${event.position}] [${event.duration}]"); - + print("Event: ${event.type}"); switch (event.type) { + case EventType.IDLE: + setState(() { + position = Duration(seconds: 0); + duration = Duration(seconds: 0); + }); + break; case EventType.BEFORE_PLAY: if (event is BeforePlayEvent) { // event.continueWithLoadingOnly(); @@ -137,32 +144,33 @@ class _SMPlayerState extends State { setState(() { position = event.position; duration = event.duration; - currentMedia = event.media; - mediaLabel = toMediaLabel(); }); } } break; - case EventType.PLAYING: + + case EventType.STATE_READY: setState(() { - currentMedia = event.media; - mediaLabel = toMediaLabel(); + _loading = false; }); break; + case EventType.PLAYING: + break; + case EventType.SET_CURRENT_MEDIA_INDEX: + setState(() { + _currentIndex = event.queuePosition; + }); + break; case EventType.PAUSED: + break; + case EventType.BUFFERING: setState(() { - currentMedia = event.media; - mediaLabel = toMediaLabel(); + _loading = true; }); break; - case EventType.NEXT: case EventType.PREVIOUS: - setState(() { - currentMedia = event.media; - mediaLabel = toMediaLabel(); - }); break; default: } @@ -174,7 +182,7 @@ class _SMPlayerState extends State { listOfMedias.addAll([media1, media2, media3, media4]); // } - player.enqueueAll(listOfMedias, autoPlay: false); + // player.enqueueAll(listOfMedias, autoPlay: false); if (!mounted) return; @@ -214,34 +222,21 @@ class _SMPlayerState extends State { void playOrPause() async { print("Player State: ${_player.state}"); - if (_player.state == PlayerState.IDLE && _player.currentMedia != null) { + if (_player.state == PlayerState.STATE_READY) { int result = await _player.play(); - if (result == Player.Ok) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Audio is now playing!!!!'))); - } - } else if (_player.state == PlayerState.BUFFERING && - _player.currentMedia != null) { - int result = await _player.play(); - if (result == Player.Ok) { + if (result == Player.ok) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Audio is now playing!!!!'))); } } else if (_player.state == PlayerState.PLAYING) { int result = await _player.pause(); - if (result == Player.Ok) { + if (result == Player.ok) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Audio is now paused!!!!'))); } } else if (_player.state == PlayerState.PAUSED) { int result = await _player.play(); - if (result == Player.Ok) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Audio is now playing again!!!!'))); - } - } else { - int? result = await _player.next(); - if (result == Player.Ok) { + if (result == Player.ok) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Audio is now playing again!!!!'))); } @@ -249,12 +244,14 @@ class _SMPlayerState extends State { } void seek(double p) { - setState(() { - position = Duration(milliseconds: p.round()); - if (_player.state != PlayerState.STOPPED) { - _player.seek(position); - } - }); + setState( + () { + position = Duration(milliseconds: p.round()); + if (_player.state != PlayerState.STOPPED) { + _player.seek(position); + } + }, + ); } void next() { @@ -327,211 +324,295 @@ class _SMPlayerState extends State { } } + Map toTXTMap(Map? txt) { + final map = {}; + txt?.forEach((key, value) { + if (key != null && value != null) { + map.putIfAbsent(key, () => value); + } + }); + + return map; + } + @override Widget build(BuildContext context) { final List colorCodes = [600, 500, 100]; - - return Container( - padding: EdgeInsets.only(top: 8.0, bottom: 0.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.delete), - onPressed: () { - _player.removeAll(); - }, - tooltip: 'Remove all', + List foundServices = []; + + return Scaffold( + drawer: Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: AppColors.primary, ), - IconButton( - icon: Icon(Icons.add), - onPressed: () { - _player.enqueueAll( - [media1, media2, media3, media4], - autoPlay: true, - ); - }, - tooltip: 'Add media', + child: Text( + 'Media Controls', + style: TextStyle( + color: Colors.white, + fontSize: 24, + ), ), - IconButton( - icon: Icon(Icons.queue), - onPressed: () { + ), + ListTile( + leading: Icon(Icons.delete), + title: Text('Remove all'), + onTap: () { + _player.stop(); + _player.removeAllMedias(); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.queue), + title: Text('Add all medias (AutoPlay)'), + onTap: () { + setState(() { _player.enqueueAll( [media1, media2, media3, media4], + autoPlay: true, ); + }); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.queue), + title: Text('Add all medias'), + onTap: () { + _player.enqueueAll( + [media1, media2, media3, media4], + ); + }, + ), + ListTile( + leading: Icon(Icons.folder), + title: Text('Add local file'), + onTap: () { + pickLocalFile(); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.play_arrow), + title: Text('Play from queue (second - 50 seconds)'), + onTap: () { + _player.playFromQueue( + 1, + loadOnly: true, + position: Duration(seconds: 50), + ); + }, + ), + ListTile( + leading: Icon(Icons.add), + title: Text('Add media1'), + onTap: () { + _player.enqueueAll([media1]); + }, + ), + ], + ), + ), + appBar: AppBar( + title: Text('SM Player'), + actions: [ + IconButton( + icon: Icon(Icons.cast), + onPressed: () { + ServiceDiscovery( + (service) { + String castId = + String.fromCharCodes(service.map['txt']['id']); + _player.cast(castId); }, - tooltip: 'Add media', - ), - IconButton( - icon: Icon(Icons.folder), - onPressed: pickLocalFile, - tooltip: 'Add local file', - ), - IconButton( - icon: Icon(Icons.play_arrow), - onPressed: () => _player.playFromQueue(1, - loadOnly: true, position: Duration(seconds: 50)), - tooltip: 'Play from queue', - ), - ], - ), - 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); - }, - ), - ), - ), - ], + ).startDiscovery(); + }, ), - Row( - children: [ - Container( - margin: EdgeInsets.only(left: 8), - child: Material( - borderRadius: BorderRadius.circular(25.0), - clipBehavior: Clip.hardEdge, - child: IconButton( - iconSize: 25, - icon: SvgPicture.asset(UIData.btPlayerSuffle, - color: _shuffled - ? AppColors.darkPink - : AppColors.primary), - onPressed: shuffleOrUnshuffle, - )), - ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - margin: EdgeInsets.only(left: 8, right: 8), - child: Material( - borderRadius: BorderRadius.circular(40.0), - clipBehavior: Clip.hardEdge, - child: IconButton( - onPressed: previous, - iconSize: 40, - icon: Container( - child: SvgPicture.asset(UIData.btPlayerPrevious), + ], + ), + body: Container( + padding: EdgeInsets.only(top: 8.0, bottom: 0.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Stack( + children: [ + Wrap( + direction: Axis.horizontal, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: EdgeInsets.only(left: 20.0), + child: Text(positionText, + style: TextStyle(fontSize: 14.0)), ), - ), + Padding( + padding: EdgeInsets.only(right: 20.0), + child: Text(durationText, + style: TextStyle(fontSize: 14.0)), + ) + ], + ), + ], + ), + Container( + width: double.infinity, + margin: EdgeInsets.only(top: 5.0), + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 2.0, + thumbShape: + const RoundSliderThumbShape(enabledThumbRadius: 7.0), + showValueIndicator: ShowValueIndicator.always, + ), + child: Slider( + activeColor: AppColors.redPink, + inactiveColor: AppColors.inactiveColor, + min: 0.0, + max: duration.inMilliseconds.toDouble(), + value: position.inMilliseconds.toDouble(), + onChanged: (double value) { + seek(value); + }, ), ), - Material( - borderRadius: BorderRadius.circular(58.0), + ), + ], + ), + Row( + children: [ + Container( + margin: EdgeInsets.only(left: 8), + child: Material( + borderRadius: BorderRadius.circular(25.0), clipBehavior: Clip.hardEdge, child: IconButton( - iconSize: 58, - icon: _player.state == PlayerState.PLAYING - ? SvgPicture.asset(UIData.btPlayerPause) - : SvgPicture.asset(UIData.btPlayerPlay), - onPressed: playOrPause, + iconSize: 25, + icon: SvgPicture.asset(UIData.btPlayerSuffle, + color: _shuffled + ? AppColors.darkPink + : AppColors.primary), + onPressed: shuffleOrUnshuffle, )), - Container( - margin: EdgeInsets.only(left: 8, right: 8), - child: Material( - borderRadius: BorderRadius.circular(40.0), - clipBehavior: Clip.hardEdge, - child: IconButton( + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only(left: 8, right: 8), + child: Material( + borderRadius: BorderRadius.circular(40.0), + clipBehavior: Clip.hardEdge, + child: IconButton( + onPressed: previous, + iconSize: 40, + icon: Container( + child: SvgPicture.asset(UIData.btPlayerPrevious), + ), + ), + ), + ), + Material( + borderRadius: BorderRadius.circular(58.0), + clipBehavior: Clip.hardEdge, + child: IconButton( + iconSize: 58, + icon: _loading + ? Container( + width: 58, + height: 58, + child: CircularProgressIndicator( + strokeWidth: 5, + ), + ) + : _player.state == PlayerState.PLAYING + ? SvgPicture.asset(UIData.btPlayerPause) + : SvgPicture.asset(UIData.btPlayerPlay), + onPressed: playOrPause, + )), + Container( + margin: EdgeInsets.only(left: 8, right: 8), + child: Material( + borderRadius: BorderRadius.circular(40.0), + clipBehavior: Clip.hardEdge, + child: IconButton( onPressed: next, iconSize: 40, icon: Container( child: SvgPicture.asset(UIData.btPlayerNext), - ))), - ), - ], - )), - Container( - margin: EdgeInsets.only(right: 8), - child: Material( - borderRadius: BorderRadius.circular(25.0), - clipBehavior: Clip.hardEdge, - child: IconButton( - iconSize: 25, - icon: SvgPicture.asset(UIData.btPlayerRepeat, - color: _repeatModeToColor()), - onPressed: _player.toggleRepeatMode, - )), - ), - ], - ), - SizedBox(height: 30), - Text(mediaLabel), - SizedBox(height: 30), - Expanded( - child: SizedBox( - height: 200, - child: ReorderableListView( - onReorder: (int oldIndex, int newIndex) { - if (newIndex > _player.items.length) { - newIndex = _player.items.length; - } - if (oldIndex < newIndex) { - newIndex--; - } - _player.reorder(oldIndex, newIndex); - }, - children: _player.items - .mapIndexed( - (index, media) => GestureDetector( - key: Key('queueItemWidgetKey$index'), - child: Container( - height: 50, - color: Colors.blue[colorCodes[index % 3]], - child: Center( - child: Text('${media.id} - ${media.name}')), + ), + ), ), - onTap: () { - _player.playFromQueue(index); - }, ), - ) - .toList(), - ), + ], + ), + ), + Container( + margin: EdgeInsets.only(right: 8), + child: Material( + borderRadius: BorderRadius.circular(25.0), + clipBehavior: Clip.hardEdge, + child: IconButton( + iconSize: 25, + icon: SvgPicture.asset(UIData.btPlayerRepeat, + color: _repeatModeToColor()), + onPressed: _player.toggleRepeatMode, + )), + ), + ], ), - ) - ], + SizedBox(height: 30), + if (_player.items.isNotEmpty) + Text('Tocando: ${_player.items[_currentIndex].name}'), + SizedBox(height: 30), + Expanded( + child: SizedBox( + height: 200, + child: ReorderableListView( + onReorder: (int oldIndex, int newIndex) { + if (newIndex > _player.items.length) { + newIndex = _player.items.length; + } + if (oldIndex < newIndex) { + newIndex--; + } + _player.reorder(oldIndex, newIndex); + }, + children: _player.items + .mapIndexed( + (index, media) => GestureDetector( + key: Key('queueItemWidgetKey$index'), + child: Container( + height: 50, + decoration: BoxDecoration( + border: index == _currentIndex + ? Border.all(color: Colors.red, width: 2.0) + : null, + color: HSLColor.fromAHSL( + 0.8, (index * 137.5) % 360, 0.7, 0.8) + .toColor(), + ), + child: Center( + child: Text('${media.id} - ${media.name}')), + ), + onTap: () { + _player.playFromQueue(index); + }, + ), + ) + .toList(), + ), + ), + ) + ], + ), ), ); } diff --git a/packages/player/ios/Classes/Logger.swift b/packages/player/ios/Classes/Logger.swift new file mode 100644 index 00000000..4cb703d6 --- /dev/null +++ b/packages/player/ios/Classes/Logger.swift @@ -0,0 +1,9 @@ +import Foundation + +public class Logger { + public static func debugLog(_ message: String) { +// #if DEBUG + print(message) +// #endif + } +} diff --git a/packages/player/ios/Classes/MessageBuffer.swift b/packages/player/ios/Classes/MessageBuffer.swift deleted file mode 100644 index 4a23b230..00000000 --- a/packages/player/ios/Classes/MessageBuffer.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// 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/NotificationManager.swift b/packages/player/ios/Classes/NotificationManager.swift new file mode 100644 index 00000000..1d9461d1 --- /dev/null +++ b/packages/player/ios/Classes/NotificationManager.swift @@ -0,0 +1,52 @@ +import Foundation +import AVFoundation + +class NotificationManager { + private weak var target: AnyObject? + + init(target: AnyObject) { + self.target = target + } + + func addAudioInterruptionObserver(selector: Selector) { + NotificationCenter.default.addObserver( + target as Any, + selector: selector, + name: AVAudioSession.interruptionNotification, + object: nil + ) + } + + func addEndPlaybackObserver(selector: Selector, for item: AVPlayerItem) { + NotificationCenter.default.addObserver( + target as Any, + selector: selector, + name: .AVPlayerItemDidPlayToEndTime, + object: item + ) + } + + func removeEndPlaybackObserver(for item: AVPlayerItem) { + NotificationCenter.default.removeObserver( + target as Any, + name: .AVPlayerItemDidPlayToEndTime, + object: item + ) + } + + func removeAllObservers() { + NotificationCenter.default.removeObserver(target as Any) + } + + func removeAudioInterruptionObserver() { + NotificationCenter.default.removeObserver( + target as Any, + name: AVAudioSession.interruptionNotification, + object: nil + ) + } + + deinit { + removeAllObservers() + } +} \ No newline at end of file diff --git a/packages/player/ios/Classes/NowPlayingInfoManager.swift b/packages/player/ios/Classes/NowPlayingInfoManager.swift new file mode 100644 index 00000000..ca60ed2e --- /dev/null +++ b/packages/player/ios/Classes/NowPlayingInfoManager.swift @@ -0,0 +1,75 @@ +import MediaPlayer +import UIKit + +class NowPlayingInfoManager { + func setupNowPlayingInfoCenter(areNotificationCommandsEnabled: @escaping () -> Bool, play: @escaping () -> Void, pause: @escaping () -> Void, nextTrack: @escaping () -> Void, previousTrack: @escaping () -> Void, seekToPosition: @escaping (Int) -> Void) { + UIApplication.shared.beginReceivingRemoteControlEvents() + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.nextTrackCommand.isEnabled = true + commandCenter.previousTrackCommand.isEnabled = true + commandCenter.pauseCommand.isEnabled = true + commandCenter.playCommand.isEnabled = true + commandCenter.changePlaybackPositionCommand.isEnabled = true + + commandCenter.pauseCommand.addTarget { _ in + if areNotificationCommandsEnabled() { + pause() + } + return .success + } + commandCenter.playCommand.addTarget { _ in + if areNotificationCommandsEnabled() { + play() + } + return .success + } + commandCenter.nextTrackCommand.addTarget { _ in + if areNotificationCommandsEnabled() { + nextTrack() + } + return .success + } + commandCenter.previousTrackCommand.addTarget { _ in + if areNotificationCommandsEnabled() { + previousTrack() + } + return .success + } + commandCenter.changePlaybackPositionCommand.addTarget { event in + if areNotificationCommandsEnabled() { + if let e = event as? MPChangePlaybackPositionCommandEvent { + seekToPosition(Int(e.positionTime * 1000)) + } + } + return .success + } + } + + func enableCommands() { + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.nextTrackCommand.isEnabled = true + commandCenter.previousTrackCommand.isEnabled = true + commandCenter.pauseCommand.isEnabled = true + commandCenter.playCommand.isEnabled = true + commandCenter.changePlaybackPositionCommand.isEnabled = true + } + + func removeNotification() { + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.nextTrackCommand.isEnabled = false + commandCenter.previousTrackCommand.isEnabled = false + commandCenter.changePlaybackPositionCommand.isEnabled = false + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + try? AVAudioSession.sharedInstance().setActive(false) + UIApplication.shared.endReceivingRemoteControlEvents() + } + + func clearNowPlayingInfo() { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + removeNotification() + } + + deinit { + removeNotification() + } +} diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index c9ea0c29..4e256de6 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -28,15 +28,13 @@ public class PlayerPlugin: NSObject, FlutterPlugin { let listMedia = batch["batch"] as? [[String: Any]] { let autoPlay = batch["autoPlay"] as? Bool ?? false let cookie = batch["cookie"] as? String ?? "" - let shouldNotifyTransition = batch["shouldNotifyTransition"] as? Bool ?? false if let mediaList = convertToMedia(mediaArray: listMedia) { - MessageBuffer.shared.send(mediaList) - smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie, shouldNotifyTransition: shouldNotifyTransition) + smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie) } } result(NSNumber(value: true)) case "next": - smPlayer?.nextTrack(from:"next call") + smPlayer?.queueManager.nextTrack() result(NSNumber(value: 1)) case "disable_notification_commands": smPlayer?.areNotificationCommandsEnabled = false @@ -49,13 +47,13 @@ public class PlayerPlugin: NSObject, FlutterPlugin { smPlayer?.removeNotification() result(NSNumber(value: 1)) case "previous": - smPlayer?.previousTrack() + smPlayer?.queueManager.previousTrack() result(NSNumber(value: 1)) case "play": - smPlayer?.play() + smPlayer?.smPlayer.play() result(NSNumber(value: 1)) case "pause": - smPlayer?.pause() + smPlayer?.smPlayer.pause() result(NSNumber(value: 1)) case "toggle_shuffle": if let args = call.arguments as? [String: Any]{ @@ -67,7 +65,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { 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) + smPlayer?.queueManager.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": @@ -78,27 +76,26 @@ public class PlayerPlugin: NSObject, FlutterPlugin { result(NSNumber(value: 1)) case "remove_in": let args = call.arguments as? [String: Any] - smPlayer?.removeByPosition(indexes:args?["indexesToDelete"] as? [Int] ?? []) + smPlayer?.queueManager.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, - let newIndex = args["newIndex"] as? Int, - let positionsList = args["positionsList"] as? [[String : Int]] { - smPlayer?.reorder(fromIndex: oldIndex, toIndex: newIndex,positionsList: positionsList) + let newIndex = args["newIndex"] as? Int { + smPlayer?.queueManager.reorder(fromIndex: oldIndex, toIndex: newIndex) } 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) + smPlayer?.queueManager.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 - smPlayer?.seekToPosition(position: position) + smPlayer?.queueManager.seekToTimePosition(position: position) } result(NSNumber(value: 1)) default: @@ -150,6 +147,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { _ = AudioSessionManager.inactivateSession() smPlayer?.clearNowPlayingInfo() smPlayer?.removeAll() + NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil) } } diff --git a/packages/player/ios/Classes/PlayerState.swift b/packages/player/ios/Classes/PlayerState.swift index 9bf7c195..5fe97534 100644 --- a/packages/player/ios/Classes/PlayerState.swift +++ b/packages/player/ios/Classes/PlayerState.swift @@ -18,6 +18,6 @@ enum PlayerState: Int { case seekEnd case bufferEmpty case itemTransition - case stateEnded case stateReady + case stateEnded } diff --git a/packages/player/ios/Classes/QueueManager.swift b/packages/player/ios/Classes/QueueManager.swift new file mode 100644 index 00000000..636a7340 --- /dev/null +++ b/packages/player/ios/Classes/QueueManager.swift @@ -0,0 +1,289 @@ +import AVFoundation + +class QueueManager { + var historyQueue = [PlaylistItem]() + var futureQueue = [PlaylistItem]() + var originalQueue = [PlaylistItem]() + var shuffledQueue = [PlaylistItem]() + + var shuffledIndices = [Int]() + var isShuffleModeEnabled = false + private let smPlayer: AVQueuePlayer + private let listeners: SMPlayerListeners + private let methodChannelManager: MethodChannelManager + + private enum Constants { + static let maxTotalItems = 5 + static let defaultTimescale: CMTimeScale = 60000 + } + + init(smPlayer: AVQueuePlayer, listeners: SMPlayerListeners, methodChannelManager: MethodChannelManager) { + self.smPlayer = smPlayer + self.listeners = listeners + self.methodChannelManager = methodChannelManager + } + + var mirrorPlayerQueue: [PlaylistItem] { + smPlayer.items().compactMap { $0.playlistItem } + } + + var fullQueue: [PlaylistItem] { + historyQueue + mirrorPlayerQueue + futureQueue + } + + func seekToTimePosition(position: Int) { + let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: Constants.defaultTimescale) + smPlayer.currentItem?.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero) { completed in + if completed { + self.methodChannelManager.notifyPlayerStateChange(state: PlayerState.seekEnd) + } + } + } + + var currentIndex: Int { + guard let currentItem = smPlayer.currentItem?.playlistItem else { return 0 } + return fullQueue.firstIndex(of: currentItem) ?? 0 + } + + func fillShuffledQueue() { + shuffledQueue = shuffledIndices.compactMap { index in + index < fullQueue.count ? fullQueue[index] : nil + } + } + + func reorder(fromIndex: Int, toIndex: Int) { + var queue = isShuffleModeEnabled ? shuffledQueue : fullQueue + queue.insert(queue.remove(at: fromIndex), at: toIndex) + distributeItemsInRightQueue(currentQueue: queue) + } + + func removeByPosition(indexes: [Int]) { + guard !indexes.isEmpty else { return } + + let sortedIndexes = indexes.sorted(by: >) + var queueAfterRemovedItems = isShuffleModeEnabled ? shuffledQueue : fullQueue + + for index in sortedIndexes where index < queueAfterRemovedItems.count { + queueAfterRemovedItems.remove(at: index) + } + + distributeItemsInRightQueue(currentQueue: queueAfterRemovedItems, keepFirst: true) + printStatus(from: "removeByPosition") + } + + 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) + } + } + + func distributeItemsInRightQueue(currentQueue: [PlaylistItem], keepFirst: Bool = true, positionArg: Int = -1, completionHandler completion: (() -> Void)? = nil) { + guard !currentQueue.isEmpty else { return } + + var position = positionArg + historyQueue.removeAll() + futureQueue.removeAll() + + if keepFirst { + position = smPlayer.currentItem?.playlistItem.flatMap { currentQueue.firstIndex(of: $0) } ?? -1 + smPlayer.items().dropFirst().forEach { smPlayer.remove($0) } + } else { + smPlayer.removeAllItems() + if position >= 0 && position < currentQueue.count { + futureQueue.append(currentQueue[position]) + } + } + + for (index, item) in currentQueue.enumerated() where index != position { + if index < position { + historyQueue.append(item) + } else { + futureQueue.append(item) + } + } + + insertIntoPlayerIfNeeded() + completion?() + } + + func insertIntoPlayerIfNeeded() { + let itemsToAdd = min(Constants.maxTotalItems - smPlayer.items().count, futureQueue.count) + guard itemsToAdd > 0 else { return } + + for _ in 0.. \(smPlayer.currentItem?.playlistItem?.title ?? "Unknown")") + printStatus(from: "insertIntoPlayerIfNeeded") + } + + func removeAll() { + smPlayer.pause() + seekToTimePosition(position: 0) + smPlayer.removeAllItems() + historyQueue.removeAll() + futureQueue.removeAll() + originalQueue.removeAll() + shuffledQueue.removeAll() + shuffledIndices.removeAll() + } + + func updateMediaUri(id: Int, uri: String?) { + guard let newUri = uri, + let index = fullQueue.firstIndex(where: { $0.mediaId == id }) else { return } + + let oldItem = fullQueue[index] + guard oldItem.url != newUri else { return } + + let updatedItem = createUpdatedPlaylistItem(from: oldItem, newUri: newUri) + var updatedQueue = fullQueue + updatedQueue[index] = updatedItem + distributeItemsInRightQueue(currentQueue: updatedQueue) + } + + private func createUpdatedPlaylistItem(from oldItem: PlaylistItem, newUri: String) -> PlaylistItem { + PlaylistItem( + albumId: oldItem.albumId, + albumName: oldItem.albumName, + title: oldItem.title, + artist: oldItem.artist, + url: newUri, + coverUrl: oldItem.coverUrl ?? "", + fallbackUrl: oldItem.fallbackUrl ?? "", + mediaId: oldItem.mediaId, + bigCoverUrl: oldItem.bigCoverUrl ?? "", + cookie: oldItem.cookie ?? "" + ) + } + + func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false) { + distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position) { + self.methodChannelManager.currentMediaIndex(index: self.currentIndex) + if timePosition > 0 { + self.seekToTimePosition(position: timePosition) + } + } + + if loadOnly { + smPlayer.pause() + } else { + smPlayer.play() + } + } + + func nextTrack() { + smPlayer.pause() + + if let currentItem = smPlayer.currentItem?.playlistItem { + historyQueue.append(currentItem) + } + + if smPlayer.currentItem?.playlistItem == fullQueue.last && smPlayer.repeatMode == .REPEAT_MODE_ALL { + playFromQueue(position: 0) + return + } + + smPlayer.advanceToNextItem() + seekToTimePosition(position: 0) + insertIntoPlayerIfNeeded() + smPlayer.play() + } + + func previousTrack() { + smPlayer.pause() + + guard let lastHistoryItem = historyQueue.popLast() else { + seekToTimePosition(position: 0) + smPlayer.play() + return + } + + guard let currentItem = smPlayer.currentItem, + let lastItemInPlayer = smPlayer.items().last else { return } + + if currentItem != lastItemInPlayer { + smPlayer.remove(lastItemInPlayer) + if let playlistItem = lastItemInPlayer.playlistItem { + futureQueue.insert(playlistItem, at: 0) + } + } + + guard let historyAVPlayerItem = createPlayerItemFromUri(lastHistoryItem.url, fallbackUrl: lastHistoryItem.fallbackUrl, cookie: lastHistoryItem.cookie) else { return } + + historyAVPlayerItem.playlistItem = lastHistoryItem + smPlayer.insert(historyAVPlayerItem, after: currentItem) + smPlayer.advanceToNextItem() + smPlayer.insert(currentItem, after: smPlayer.currentItem) + + seekToTimePosition(position: 0) + insertIntoPlayerIfNeeded() + listeners.addItemsObservers() + smPlayer.play() + } + + func getCurrentPlaylistItem() -> PlaylistItem? { + smPlayer.currentItem?.playlistItem + } + + func printStatus(from: String) { + Logger.debugLog("QueueActivity #################################################") + Logger.debugLog("QueueActivity \(from) ") + Logger.debugLog("QueueActivity Current Index: \(currentIndex)") + Logger.debugLog("QueueActivity ------------------------------------------") + Logger.debugLog("QueueActivity printStatus History: \(historyQueue.count) items") + + historyQueue.forEach { item in + Logger.debugLog("QueueActivity printStatus History: \(item.title)") + } + + Logger.debugLog("QueueActivity printStatus ------------------------------------------") + Logger.debugLog("QueueActivity printStatus futureQueue Items: \(futureQueue.count) items") + + futureQueue.forEach { item in + Logger.debugLog("QueueActivity printStatus Upcoming: \(item.title)") + } + + Logger.debugLog("QueueActivity printStatus ------------------------------------------") + Logger.debugLog("QueueActivity printStatus AVQueuePlayer items: \(smPlayer.items().count)") + + smPlayer.items().forEach { item in + Logger.debugLog("QueueActivity printStatus AVQueuePlayer: \(item.playlistItem?.title ?? "Unknown")") + } + + Logger.debugLog("QueueActivity printStatus #################################################") + } + + func enqueue(item: PlaylistItem) { + futureQueue.append(item) + } + + func createPlayerItemFromUri(_ uri: String?, fallbackUrl: String?, cookie: String?) -> AVPlayerItem? { + let urlString = uri ?? fallbackUrl ?? "" + guard !urlString.isEmpty else { return nil } + + if urlString.contains("https") { + guard let url = URL(string: urlString) else { return nil } + let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie ?? ""]] + return AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) + } else { + return AVPlayerItem(asset: AVAsset(url: URL(fileURLWithPath: urlString))) + } + } + + deinit { + removeAll() + } +} diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 7827fa55..f3e36fb4 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -1,6 +1,5 @@ import Foundation import AVFoundation -import MediaPlayer private var playlistItemKey: UInt8 = 0 var currentRepeatmode: AVQueuePlayer.RepeatMode = .REPEAT_MODE_OFF @@ -8,29 +7,22 @@ public class SMPlayer : NSObject { var methodChannelManager: MethodChannelManager? private var cookie: String = "" //Queue handle - private var smPlayer: AVQueuePlayer - 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 smPlayer: AVQueuePlayer + var queueManager: QueueManager! private var listeners: SMPlayerListeners? = nil - private var seekToLoadOnly: Bool = false // Transition Control - private var shouldNotifyTransition: Bool = false var areNotificationCommandsEnabled: Bool = true - var fullQueue: [AVPlayerItem] { - return historyQueue + smPlayer.items() + futureQueue + private var notificationManager: NotificationManager! + private var nowPlayingInfoManager: NowPlayingInfoManager! + + + var fullQueue: [PlaylistItem] { + return queueManager.fullQueue } var currentIndex : Int { - guard let currentItem = smPlayer.currentItem else { - return 0 - } - return fullQueue.firstIndex(of: currentItem) ?? 0 + return queueManager.currentIndex } init(methodChannelManager: MethodChannelManager?) { @@ -38,48 +30,64 @@ public class SMPlayer : NSObject { super.init() 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 && shouldNotifyTransition){ - methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) - } - shouldNotifyTransition = true - self.updateEndPlaybackObserver() - seekToLoadOnly = !seekToLoadOnly - self.listeners?.addItemsObservers() - if(seekToLoadOnly){ - seekToLoadOnly = false - methodChannelManager?.currentMediaIndex(index: self.currentIndex) - } - } + queueManager = QueueManager(smPlayer: smPlayer,listeners:listeners!, methodChannelManager: methodChannelManager!) + nowPlayingInfoManager = NowPlayingInfoManager() + notificationManager = NotificationManager(target: self) + + + notificationManager.addAudioInterruptionObserver(selector: #selector(handleInterruption(_:))) + listeners?.onMediaChanged = { [weak self] shouldNotify in + guard let self = self else { return } + guard self.smPlayer.items().count > 0 else { return } + let isNotFirstItem = self.smPlayer.currentItem != self.fullQueue.first + let hasHistory = self.queueManager.historyQueue.count > 0 + if isNotFirstItem && hasHistory && shouldNotify { + self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) + } + self.updateEndPlaybackObserver() + self.listeners?.addItemsObservers() + self.methodChannelManager?.currentMediaIndex(index: self.currentIndex) } - setupNowPlayingInfoCenter() + nowPlayingInfoManager.setupNowPlayingInfoCenter( + areNotificationCommandsEnabled: { [weak self] in self?.areNotificationCommandsEnabled ?? true }, + play: { [weak self] in self?.smPlayer.play() }, + pause: { [weak self] in self?.smPlayer.pause() }, + nextTrack: { [weak self] in self?.queueManager.nextTrack() }, + previousTrack: { [weak self] in self?.queueManager.previousTrack() }, + seekToPosition: { [weak self] pos in self?.queueManager.seekToTimePosition(position: pos) } + ) _ = AudioSessionManager.activeSession() } - - func pause() { - smPlayer.pause() + + @objc private func handleInterruption(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { + return + } + switch type { + case .began: + smPlayer.pause() + case .ended: + if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { + let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) + if options.contains(.shouldResume) { + smPlayer.play() + } + } + @unknown default: + break + } } func addEndPlaybackObserver() { guard let currentItem = smPlayer.currentItem else { return } - NotificationCenter.default.addObserver( - self, - selector: #selector(itemDidFinishPlaying(_:)), - name: .AVPlayerItemDidPlayToEndTime, - object: currentItem - ) + notificationManager.addEndPlaybackObserver(selector: #selector(itemDidFinishPlaying(_:)), for: currentItem) } func removeEndPlaybackObserver() { if let currentItem = smPlayer.currentItem { - NotificationCenter.default.removeObserver( - self, - name: .AVPlayerItemDidPlayToEndTime, - object: currentItem - ) + notificationManager.removeEndPlaybackObserver(for: currentItem) } } @@ -111,371 +119,91 @@ public class SMPlayer : NSObject { smPlayer.pause() smPlayer.replaceCurrentItem(with: nil) clearNowPlayingInfo() + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.idle) } func clearNowPlayingInfo() { - MPNowPlayingInfoCenter.default().nowPlayingInfo = nil - removeNotification() + nowPlayingInfoManager.clearNowPlayingInfo() } - func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String, shouldNotifyTransition: Bool) { - var playerItem: AVPlayerItem? - guard let message = MessageBuffer.shared.receive() else { return } - self.shouldNotifyTransition = shouldNotifyTransition + func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { 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": self.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!) + for media in medias { + media.cookie = self.cookie + queueManager.enqueue(item: media) } - insertIntoPlayerIfNeeded() + queueManager.insertIntoPlayerIfNeeded() 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]) { - 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 { - 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]) - } - } - } - - - 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(from:String) { - smPlayer.pause() - print("#print nextTrack \(from)") - 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) - insertIntoPlayerIfNeeded() - smPlayer.play() - printStatus(from:"NEXT") - } - - func previousTrack() { - 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) - - seekToPosition(position: 0) - insertIntoPlayerIfNeeded() - smPlayer.play() - printStatus(from:"previousTrack") + queueManager.toggleShuffle(positionsList: positionsList) + methodChannelManager?.shuffleChanged(shuffleIsActive: queueManager.isShuffleModeEnabled) } func setNowPlaying(){ - NowPlayingCenter.set(item: getCurrentPlaylistItem()) - } - - private func insertIntoPlayerIfNeeded() { - let maxTotalItems = 5 - let itemsToAdd = min(maxTotalItems - smPlayer.items().count, futureQueue.count) - - for _ in 0.. \(String(describing: smPlayer.currentItem?.playlistItem?.title))") - printStatus(from:"insertIntoPlayerIfNeeded") + NowPlayingCenter.set(item: queueManager.getCurrentPlaylistItem()) } func removeAll(){ - smPlayer.pause() - smPlayer.seek(to: CMTime.zero) - smPlayer.removeAllItems() - historyQueue.removeAll() - futureQueue.removeAll() - originalQueue.removeAll() - shuffledQueue.removeAll() - shuffledIndices.removeAll() + queueManager.removeAll() + methodChannelManager?.notifyPlayerStateChange(state:PlayerState.idle) } 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(){ - smPlayer.play() + nowPlayingInfoManager.removeNotification() } - func seekToPosition(position:Int){ - let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: 60000) - smPlayer.currentItem?.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { completed in - if completed { - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.seekEnd) - } - } ) - } - - func getCurrentPlaylistItem() -> PlaylistItem? { - guard let currentItem = smPlayer.currentItem else { - return nil - } - return currentItem.playlistItem - } - - private func distributeItemsInRightQueue(currentQueue: [AVPlayerItem], keepFirst: Bool = true, positionArg: Int = -1, completionHandler completion: (() -> Void)? = nil) { - guard currentQueue.count > 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() - for item in itemsToRemove { - smPlayer.remove(item) - } - }else{ - smPlayer.removeAllItems() - futureQueue.append(currentQueue[position]) - } - - for (index, item) in currentQueue.enumerated() { - if(index != position){ - if index < position { - historyQueue.append(item) - } else { - futureQueue.append(item) - } - } - } - insertIntoPlayerIfNeeded() - 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 - listeners?.mediaChange?.invalidate() - } - listeners?.removeItemObservers() - distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position, completionHandler: { - print("#NATIVE LOGS ==> completionHandler") - self.methodChannelManager?.currentMediaIndex(index: self.currentIndex) - if(timePosition > 0){ - self.seekToPosition(position: timePosition) - } - }) - if(loadOnly){ - pause() - }else{ - play() - } - 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() - commandCenter.nextTrackCommand.isEnabled = true; - commandCenter.previousTrackCommand.isEnabled = true; - commandCenter.changePlaybackPositionCommand.isEnabled = true - - commandCenter.pauseCommand.addTarget { [self]event in - if(areNotificationCommandsEnabled){ - smPlayer.pause() - } - return .success - } - - commandCenter.playCommand.addTarget { [self]event in - if(areNotificationCommandsEnabled){ - smPlayer.play() - } - return .success - } - - commandCenter.nextTrackCommand.addTarget {[self]event in - if(areNotificationCommandsEnabled){ - nextTrack(from: "commandCenter.nextTrackCommand") - } - return .success - } - commandCenter.previousTrackCommand.addTarget {[self]event in - if(areNotificationCommandsEnabled){ - previousTrack() - } - return .success - } - - commandCenter.changePlaybackPositionCommand.addTarget{[self]event in - if(areNotificationCommandsEnabled){ - let e = event as? MPChangePlaybackPositionCommandEvent - seekToPosition(position: Int((e?.positionTime ?? 0) * 1000)) - } - return .success - } + nowPlayingInfoManager.enableCommands() } - func printStatus(from:String) { - if(isDebugMode()){ - 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("QueueActivity printStatus History: \(String(describing: item.playlistItem?.title))") - } - print("QueueActivity printStatus ------------------------------------------") - print("QueueActivity printStatus futureQueue Items: \(futureQueue.count) items") - - for item in futureQueue { - print("QueueActivity printStatus Upcoming: \(String(describing: item.playlistItem?.title))") - } - print("QueueActivity printStatus ------------------------------------------") - print("QueueActivity printStatus AVQueuePlayer items: \(smPlayer.items().count)") - - for item in smPlayer.items() { - print("QueueActivity printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") - } - print("QueueActivity printStatus #################################################") - } - } - - func isDebugMode() -> Bool { - #if DEBUG - return true - #else - return false - #endif - } - - //override automatic next @objc func itemDidFinishPlaying(_ notification: Notification) { - pause() + smPlayer.pause() switch smPlayer.repeatMode { case .REPEAT_MODE_ALL: if(smPlayer.currentItem == fullQueue.last){ - playFromQueue(position: 0) + queueManager.playFromQueue(position: 0) break } - nextTrack(from:"REPEAT_MODE_ALL") + queueManager.nextTrack() case .REPEAT_MODE_ONE: - seekToPosition(position: 0) + queueManager.seekToTimePosition(position: 0) case .REPEAT_MODE_OFF: - nextTrack(from: "REPEAT_MODE_OFF") + queueManager.nextTrack() + } + smPlayer.play() + } + + private func notifyMediaChangedIfNeeded() { + let isNotFirstItem = smPlayer.currentItem != fullQueue.first + let hasHistory = queueManager.historyQueue.count > 0 + + if isNotFirstItem && hasHistory { + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) } - play() + } + + deinit { + removeEndPlaybackObserver() + notificationManager.removeAudioInterruptionObserver() + listeners?.removePlayerObservers() + listeners = nil + queueManager.removeAll() + nowPlayingInfoManager.removeNotification() + clearNowPlayingInfo() + smPlayer.pause() + smPlayer.removeAllItems() } } @@ -520,5 +248,4 @@ public extension AVQueuePlayer { 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 e399816b..57f48ae9 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -1,145 +1,208 @@ import Foundation import AVFoundation -public class SMPlayerListeners : NSObject { +public class SMPlayerListeners: NSObject { let smPlayer: AVQueuePlayer - let methodChannelManager: MethodChannelManager? + weak var methodChannelManager: MethodChannelManager? - var onMediaChanged: (() -> Void)? + var onMediaChanged: ((Bool) -> Void)? + private var itemObservations = Set() + private var playerObservations = Set() + private var periodicTimeObserver: Any? + private var notificationObservers = [NSObjectProtocol]() + + private var lastState = PlayerState.idle + private var lastNotificationTime = Date() + private let notificationThrottleInterval: TimeInterval = 0.1 + + private let positionUpdateInterval: TimeInterval = 0.8 init(smPlayer: AVQueuePlayer, methodChannelManager: MethodChannelManager?) { self.smPlayer = smPlayer self.methodChannelManager = methodChannelManager super.init() addPlayerObservers() + addItemsObservers() } - 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 addItemsObservers() { - removeItemObservers() + cleanupItemObservers() guard let currentItem = smPlayer.currentItem else { return } - statusChange = currentItem.observe(\.status, options: [.new, .old]) { (playerItem, change) in - if playerItem.status == .failed { - if let error = playerItem.error { - self.methodChannelManager?.notifyError(error: "UNKNOW ERROR") - } + + let statusObservation = currentItem.observe( + \AVPlayerItem.status, + options: [.new, .initial] + ) { [weak self] playerItem, _ in + switch playerItem.status { + case .failed: + let errorMessage = playerItem.error?.localizedDescription ?? "Unknown playback error" + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] ERROR: \(errorMessage)") + self?.methodChannelManager?.notifyError(error: errorMessage) + case .readyToPlay: + self?.notifyPlayerStateChange(state: .stateReady) + case .unknown: + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Player status unknown") + @unknown default: + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Player status unknown default") } } - - loading = currentItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old]) { [weak self] (new, old) in - guard let self = self else { return } - print("#NATIVE LOGS ==> Listeners - observer - loading") - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + itemObservations.insert(statusObservation) + + let bufferEmptyObservation = currentItem.observe( + \AVPlayerItem.isPlaybackBufferEmpty, + options: [.new] + ) { [weak self] _, change in + if change.newValue == true { + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Buffering (buffer empty)") + self?.notifyPlayerStateChange(state: .buffering) + } } + itemObservations.insert(bufferEmptyObservation) - loaded = currentItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { (player, _) in - print("#NATIVE LOGS ==> Listeners - observer - loaded") + let bufferKeepUpObservation = currentItem.observe( + \AVPlayerItem.isPlaybackLikelyToKeepUp, + options: [.new] + ) { _, change in + if change.newValue == true { + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Buffer ready (likely to keep up)") + } } + itemObservations.insert(bufferKeepUpObservation) + + let observer = NotificationCenter.default.addObserver( + forName: .AVPlayerItemFailedToPlayToEndTime, + object: currentItem, + queue: .main + ) { [weak self] notification in + if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error { + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Player Item Error: \(error.localizedDescription)") + self?.methodChannelManager?.notifyError(error: error.localizedDescription) + } + } + notificationObservers.append(observer) } - - 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 - print("#NATIVE LOGS ==> onMediaChanged: \(oldItemExists)") - + func notifyPlayerStateChange(state: PlayerState) { + let now = Date() + guard lastState != state && (now.timeIntervalSince(lastNotificationTime) >= notificationThrottleInterval || lastState != state) else { + return + } + + lastNotificationTime = now + methodChannelManager?.notifyPlayerStateChange(state: state) + lastState = state + } + + func addMediaChangeObserver() { + let mediaChangeObservation = smPlayer.observe( + \AVQueuePlayer.currentItem, + options: [.new, .old] + ) { [weak self] _, change in if let newItem = change.newValue, newItem != change.oldValue { - self.onMediaChanged?() - self.addItemsObservers() + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Media changed") + self?.onMediaChanged?(true) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.addItemsObservers() + } } } + playerObservations.insert(mediaChangeObservation) } + func addPlayerObservers() { addMediaChangeObserver() + addPeriodicTimeObserver() - 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) - } - } - } - - notPlayingReason = smPlayer.observe(\.reasonForWaitingToPlay, options: [.new]) { (playerItem, change) in - switch self.smPlayer.reasonForWaitingToPlay { - case .evaluatingBufferingRate: - print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - evaluatingBufferingRate") - case .toMinimizeStalls: - print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - toMinimizeStalls") - case .noItemToPlay: - print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - noItemToPlay") - default: - print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - default") + let reasonObservation = smPlayer.observe( + \AVQueuePlayer.reasonForWaitingToPlay, + options: [.new] + ) { player, _ in + if let reason = player.reasonForWaitingToPlay { + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Waiting reason: \(String(describing: reason))") } } + playerObservations.insert(reasonObservation) - playback = smPlayer.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] (player, change) in - guard let self = self else { return } + let playbackObservation = smPlayer.observe( + \AVQueuePlayer.timeControlStatus, + options: [.new, .old] + ) { [weak self] player, _ in switch player.timeControlStatus { case .playing: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.playing) - print("#NATIVE LOGS ==> Listeners - Playing") + self?.notifyPlayerStateChange(state: .playing) + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Playing") case .paused: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.paused) - print("#NATIVE LOGS ==> Listeners - Paused") + self?.notifyPlayerStateChange(state: .paused) + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Paused") case .waitingToPlayAtSpecifiedRate: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) - print("#NATIVE LOGS ==> Listeners - Buffering") + self?.notifyPlayerStateChange(state: .buffering) + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Buffering") @unknown default: - break + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Unknown time control status") } } + playerObservations.insert(playbackObservation) } - func removeItemObservers() { - statusChange?.invalidate() - loading?.invalidate() - loaded?.invalidate() + private func addPeriodicTimeObserver() { + removePeriodicTimeObserver() - statusChange = nil - loading = nil - loaded = nil + let interval = CMTime(seconds: positionUpdateInterval, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + periodicTimeObserver = smPlayer.addPeriodicTimeObserver( + forInterval: interval, + queue: .main + ) { [weak self] _ in + self?.handlePeriodicTimeUpdate() + } + } + + private func handlePeriodicTimeUpdate() { + let position = CMTimeGetSeconds(smPlayer.currentTime()) + + guard let currentItem = smPlayer.currentItem else { return } + let duration = CMTimeGetSeconds(currentItem.duration) + + guard duration.isFinite && duration > 0 && position.isFinite && position >= 0 && position <= duration else { + return + } - removeErrorObserver() + methodChannelManager?.notifyPositionChange(position: position, duration: duration) + + if let playlistItem = currentItem.playlistItem { + NowPlayingCenter.update(item: playlistItem, rate: 1.0, position: position, duration: duration) + } } - func removeErrorObserver() { - if let currentItem = smPlayer.currentItem { - NotificationCenter.default.removeObserver( - self, - name: .AVPlayerItemFailedToPlayToEndTime, - object: currentItem - ) + private func removePeriodicTimeObserver() { + if let observer = periodicTimeObserver { + smPlayer.removeTimeObserver(observer) + periodicTimeObserver = nil } } - + private func cleanupItemObservers() { + itemObservations.forEach { $0.invalidate() } + itemObservations.removeAll() + cleanupNotificationObservers() + } + + private func cleanupNotificationObservers() { + notificationObservers.forEach { observer in + NotificationCenter.default.removeObserver(observer) + } + notificationObservers.removeAll() + } + func removePlayerObservers() { - notPlayingReason?.invalidate() - playback?.invalidate() - mediaChange?.invalidate() - - mediaChange = nil - notPlayingReason = nil - playback = nil + playerObservations.forEach { $0.invalidate() } + playerObservations.removeAll() + removePeriodicTimeObserver() + cleanupItemObservers() } deinit { removePlayerObservers() - removeItemObservers() + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Deinitializing") } } + diff --git a/packages/player/lib/player.dart b/packages/player/lib/player.dart index 4aeb277f..137a487b 100644 --- a/packages/player/lib/player.dart +++ b/packages/player/lib/player.dart @@ -1,11 +1,10 @@ -export 'src/media.dart'; -export 'src/player_state.dart'; -export 'src/release_mode.dart'; -export 'src/player.dart'; -export 'src/event_type.dart'; -export 'src/event.dart'; -export 'src/duration_change_event.dart'; -export 'src/position_change_event.dart'; -export 'src/before_play_event.dart'; -export 'src/current_queue_updated.dart'; -export 'src/repeat_mode.dart'; +export 'src/models/media.dart'; +export 'src/enums/player_state.dart'; +export 'src/core/player.dart'; +export 'src/enums/event_type.dart'; +export 'src/models/event.dart'; +export 'src/events/duration_change_event.dart'; +export 'src/events/position_change_event.dart'; +export 'src/events/before_play_event.dart'; +export 'src/events/current_queue_updated.dart'; +export 'src/enums/repeat_mode.dart'; diff --git a/packages/player/lib/src/core/player.dart b/packages/player/lib/src/core/player.dart new file mode 100644 index 00000000..3bae5676 --- /dev/null +++ b/packages/player/lib/src/core/player.dart @@ -0,0 +1,525 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:smaws/aws.dart'; +import 'package:smplayer/src/core/player_channel.dart'; +import 'package:smplayer/src/core/player_event_controller.dart'; +import 'package:smplayer/src/models/event.dart'; +import 'package:smplayer/src/enums/event_type.dart'; +import 'package:smplayer/src/events/position_change_event.dart'; +import 'package:smplayer/src/models/media.dart'; +import 'package:smplayer/src/models/previous_playlist_model.dart'; +import 'package:smplayer/src/queue/queue.dart'; +import 'package:smplayer/src/enums/repeat_mode.dart'; +import 'package:smplayer/src/services/isar_service.dart'; + +import '../enums/player_state.dart'; + +class Player { + Player({ + required this.playerId, + required this.cookieSigner, + required this.localMediaValidator, + this.initializeIsar = false, + this.autoPlay = false, + }) { + _queue = Queue( + beforeInitialize: () async => + await _playerChannel.invokeMethod('remove_all'), + initializeIsar: this.initializeIsar, + onInitialize: (List items) async { + await enqueueAll(items, alreadyAddedToStorage: true); + }, + ); + eventController = PlayerEventController(); + } + + // Static variables + static bool logEnabled = false; + static RepeatMode _repeatMode = RepeatMode.REPEAT_MODE_OFF; + static bool _shuffleEnabled = false; + static const ok = PlayerChannel.ok; + static const notOk = PlayerChannel.notOk; + + // Required constructor parameters + final String playerId; + final Future Function() cookieSigner; + final String? Function(Media)? localMediaValidator; + final bool autoPlay; + + // State variables + bool initializeIsar; + bool externalPlayback = false; + PlayerState state = PlayerState.IDLE; + CookiesForCustomPolicy? _cookies; + + // Queue and event management + late Queue _queue; + late final PlayerChannel _playerChannel = PlayerChannel(this); + late final PlayerEventController eventController; + + // ChromeCast related + final chromeCastEnabledEvents = [ + EventType.BEFORE_PLAY, + EventType.NEXT, + EventType.PREVIOUS, + EventType.POSITION_CHANGE, + EventType.REWIND, + EventType.PLAY_REQUESTED, + EventType.PAUSED, + EventType.PLAYING, + EventType.EXTERNAL_RESUME_REQUESTED, + EventType.EXTERNAL_PAUSE_REQUESTED, + EventType.SET_CURRENT_MEDIA_INDEX, + ]; + + // Getters and setters + bool get shallSendEvents => eventController.shallSendEvents; + set shallSendEvents(bool value) => eventController.shallSendEvents = value; + bool get itemsReady => _queue.itemsReady; + bool get isShuffleEnabled => _shuffleEnabled; + RepeatMode get repeatMode => _repeatMode; + set shuffleEnabled(bool value) => _shuffleEnabled = value; + set repeatMode(RepeatMode value) => _repeatMode = value; + Stream get onEvent => eventController.onEvent; + + // ================ Queue Getters ================ + Media? get currentMedia => _queue.current; + int get previousPlaylistIndex => _queue.previousIndex; + PreviousPlaylistPosition? get previousPlaylistPosition => + _queue.previousPosition; + List get items => _queue.items; + int get size => items.length; + int get currentIndex => _queue.index; + + // ================ Queue Management Methods ================ + set queuePosition(int position) { + _queue.setIndex = position; + } + + Future enqueueAll( + List items, { + bool autoPlay = false, + bool saveOnTop = false, + bool alreadyAddedToStorage = false, + }) async { + if (!alreadyAddedToStorage) { + _queue.addAll(items, saveOnTop: saveOnTop); + } + + if (_cookies == null || !_cookies!.isValid) { + _log("Generating Cookies"); + _cookies = await cookieSigner(); + } + + String cookie = _cookies!.toHeaders(); + final int batchSize = 80; + final List> batchArgs = items.map((media) { + final localPath = localMediaValidator?.call(media); + return {...media.copyWith(url: localPath ?? media.url).toJson()}; + }).toList(); + + for (int i = 0; i < batchArgs.length; i += batchSize) { + final batch = batchArgs.sublist(i, min(i + batchSize, batchArgs.length)); + unawaited( + _playerChannel.invokeMethod('enqueue', { + 'batch': batch, + 'autoPlay': autoPlay, + 'playerId': playerId, + 'shallSendEvents': shallSendEvents, + 'externalplayback': externalPlayback, + if (i == 0) ...{'cookie': cookie}, + }), + ); + } + + return PlayerChannel.ok; + } + + int removeByPosition({required List positionsToDelete}) { + _playerChannel.invokeMethod('remove_in', { + 'indexesToDelete': positionsToDelete, + }); + return _queue.removeByPosition( + positionsToDelete: positionsToDelete, + isShuffle: isShuffleEnabled, + ); + } + + Future removeAllMedias() async { + _queue.clear(); + queuePosition = 0; + await IsarService.instance.removeAllMusics(); + _playerChannel.invokeMethod('remove_all'); + return PlayerChannel.ok; + } + + Future reorder(int oldIndex, int newIndex) async { + _queue.reorder(oldIndex, newIndex, isShuffleEnabled); + debugPrint('#queue.reorder: ${getPositionsList()}'); + _playerChannel.invokeMethod('reorder', { + 'oldIndex': oldIndex, + 'newIndex': newIndex, + 'positionsList': getPositionsList(), + }); + return PlayerChannel.ok; + } + + Future clear() async => removeAllMedias(); + + Future restartQueue() async { + final media = _queue.restart(); + return media; + } + + // ================ Playback Control Methods ================ + Future play() async { + await _invokeMethodWithDefaultArgs('play'); + return PlayerChannel.ok; + } + + Future pause() async { + _notifyPlayerStatusChangeEvent(EventType.PAUSE_REQUEST); + return await _invokeMethodWithDefaultArgs('pause'); + } + + Future stop() async { + _notifyPlayerStatusChangeEvent(EventType.STOP_REQUESTED); + final int result = await _invokeMethodWithDefaultArgs('stop'); + + if (result == PlayerChannel.ok) { + state = PlayerState.STOPPED; + _notifyPlayerStatusChangeEvent(EventType.STOPPED); + } + + return result; + } + + Future release() async { + _notifyPlayerStatusChangeEvent(EventType.RELEASE_REQUESTED); + final int result = await _invokeMethodWithDefaultArgs('release'); + + if (result == PlayerChannel.ok) { + state = PlayerState.STOPPED; + _notifyPlayerStatusChangeEvent(EventType.RELEASED); + } + _queue.dispose(); + return result; + } + + Future seek(Duration position, {bool playWhenReady = true}) { + _notifyPlayerStateChangeEvent(this, EventType.SEEK_START, ""); + return _invokeMethodWithDefaultArgs('seek', { + 'position': position.inMilliseconds, + 'playWhenReady': playWhenReady, + }); + } + + Future setVolume(double volume) { + return _invokeMethodWithDefaultArgs('setVolume', {'volume': volume}); + } + + Future getDuration() { + return _invokeMethodWithDefaultArgs('getDuration'); + } + + Future getCurrentPosition() async { + return _invokeMethodWithDefaultArgs('getCurrentPosition'); + } + + // ================ Queue Navigation Methods ================ + Future previous({bool isFromChromecast = false}) async { + if (_queue.shouldRewind()) { + seek(Duration(milliseconds: 0)); + print("#APP LOGS ==> shouldRewind"); + return PlayerChannel.ok; + } + + Media? media = _queue.possiblePrevious(); + if (isFromChromecast && media != null) { + return _queue.items.indexOf(media); + } + if (media == null) { + return null; + } + if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { + setRepeatMode("all"); + } + return await _invokeMethodWithDefaultArgs('previous'); + } + + Future next({bool isFromChromecast = false}) async { + final media = _queue.possibleNext(repeatMode); + if (isFromChromecast && media != null) { + return _queue.items.indexOf(media); + } + if (media != null) { + if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { + setRepeatMode("all"); + } + return _invokeMethodWithDefaultArgs('next'); + } else { + return null; + } + } + + Future forward() async { + if (currentMedia == null) { + return PlayerChannel.notOk; + } + return _forward(currentMedia); + } + + Future _forward(Media? media) async { + if (media == null) { + return PlayerChannel.notOk; + } + final duration = Duration(milliseconds: await getDuration()); + _notifyPositionChangeEvent(this, duration, duration); + _notifyForwardEvent(media); + return stop(); + } + + Future playFromQueue( + int pos, { + Duration? position, + bool loadOnly = false, + }) async { + if (!loadOnly) { + _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); + } + if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { + setRepeatMode("all"); + } + return _playerChannel.invokeMethod('playFromQueue', { + 'position': pos, + 'timePosition': position?.inMilliseconds, + 'loadOnly': loadOnly, + }); + } + + // ================ Repeat and Shuffle Methods ================ + Future toggleRepeatMode() async { + return _playerChannel.invokeMethod('repeat_mode'); + } + + Future setRepeatMode(String mode) async { + return _playerChannel.invokeMethod('set_repeat_mode', {'mode': mode}); + } + + Future disableRepeatMode() async { + return _playerChannel.invokeMethod('disable_repeat_mode'); + } + + Future toggleShuffle() async { + if (!isShuffleEnabled) { + _queue.shuffle(); + } else { + _queue.unshuffle(); + } + debugPrint('#queue.shuffle: ${getPositionsList()}'); + _playerChannel.invokeMethod('toggle_shuffle', { + 'positionsList': getPositionsList(), + }); + } + + // ================ Notification Methods ================ + Future removeNotification() async { + await _playerChannel.invokeMethod('remove_notification'); + return PlayerChannel.ok; + } + + Future disableNotificatonCommands() async { + await _invokeMethodWithDefaultArgs('disable_notification_commands'); + return PlayerChannel.ok; + } + + Future enableNotificatonCommands() async { + await _invokeMethodWithDefaultArgs('enable_notification_commands'); + return PlayerChannel.ok; + } + + Future notifyAdsPlaying() async { + await _invokeMethodWithDefaultArgs('ads_playing'); + return PlayerChannel.ok; + } + + // ================ Event Management Methods ================ + int enableEvents() { + shallSendEvents = true; + return PlayerChannel.ok; + } + + int disableEvents() { + shallSendEvents = false; + return PlayerChannel.ok; + } + + // ================ Media Management Methods ================ + Future updateMediaUri({required int id, String? uri}) async { + _playerChannel.invokeMethod('update_media_uri', {'id': id, 'uri': uri}); + return PlayerChannel.ok; + } + + Future updateFavorite({ + required bool isFavorite, + required int id, + }) async { + return _playerChannel.invokeMethod('update_favorite', { + 'isFavorite': isFavorite, + 'idFavorite': id, + }); + } + + Future cast(String castId) async { + await _playerChannel.invokeMethod('cast', {'castId': castId}); + return PlayerChannel.ok; + } + + // ================ Queue State Methods ================ + Media? possibleNext(RepeatMode repeatMode) { + return _queue.possibleNext(repeatMode); + } + + Media? possiblePrevious() { + return _queue.possiblePrevious(); + } + + void setIndexAndUpdateIsar(int index) { + _queue.setIndex = index; + if (currentMedia != null) { + _queue.updateIsarIndex(currentMedia!.id, currentIndex); + } + } + + List> getPositionsList() { + return [ + for (var item in _queue.playerQueue) + {'originalPosition': item.originalPosition}, + ]; + } + + // ================ Event Notification Methods ================ + void _notifyPositionChangeEvent( + Player player, + Duration newPosition, + Duration newDuration, + ) { + final media = _queue.current; + final currentIndex = _queue.index; + if (media != null) { + final position = newPosition.inSeconds; + eventController.add( + PositionChangeEvent( + media: media, + queuePosition: currentIndex, + position: newPosition, + duration: newDuration, + ), + ); + if (position >= 0 && position % 5 == 0) { + unawaited( + IsarService.instance.addPreviousPlaylistPosition( + PreviousPlaylistPosition( + mediaId: media.id, + position: newPosition.inMilliseconds.toDouble(), + duration: newDuration.inMilliseconds.toDouble(), + ), + ), + ); + } + } + } + + void _notifyPlayerStateChangeEvent( + Player player, + EventType eventType, + String error, + ) { + final currentIndex = _queue.index; + if (error.isNotEmpty) { + _notifyPlayerErrorEvent( + player: player, + error: error, + errorType: PlayerErrorType.INFORMATION, + ); + } + if (_queue.current != null) { + eventController.add( + Event( + type: eventType, + media: _queue.current!, + queuePosition: currentIndex, + ), + ); + } + } + + void _notifyPlayerErrorEvent({ + required Player player, + required String error, + PlayerErrorType? errorType, + }) { + final currentIndex = _queue.index; + if (_queue.current != null) { + eventController.add( + Event( + type: EventType.ERROR_OCCURED, + media: _queue.current!, + queuePosition: currentIndex, + error: error, + errorType: errorType ?? PlayerErrorType.UNDEFINED, + ), + ); + } + } + + void _notifyForwardEvent(Media media) async { + final positionInMilli = await getCurrentPosition(); + final durationInMilli = await getDuration(); + + eventController.add( + Event( + type: EventType.FORWARD, + media: media, + queuePosition: currentIndex, + position: Duration(milliseconds: positionInMilli), + duration: Duration(milliseconds: durationInMilli), + ), + ); + } + + void _notifyPlayerStatusChangeEvent(EventType type) { + if (currentMedia != null) { + eventController.add( + Event(type: type, media: currentMedia!, queuePosition: currentIndex), + ); + } + } + + // ================ Utility Methods ================ + void _log(String param) { + debugPrint(param); + } + + Future _invokeMethodWithDefaultArgs( + String method, [ + Map? arguments, + ]) async { + if (!shallSendEvents) { + return PlayerChannel.notOk; + } + arguments ??= const {}; + final Map args = Map.of(arguments) + ..['playerId'] = playerId + ..['shallSendEvents'] = shallSendEvents + ..['externalplayback'] = externalPlayback; + + return _playerChannel.invokeMethod(method, args); + } + + Future dispose() async { + await eventController.dispose(); + _queue.dispose(); + } +} diff --git a/packages/player/lib/src/core/player_channel.dart b/packages/player/lib/src/core/player_channel.dart new file mode 100644 index 00000000..4ccf5012 --- /dev/null +++ b/packages/player/lib/src/core/player_channel.dart @@ -0,0 +1,342 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; +import 'package:smplayer/src/models/event.dart'; +import 'package:smplayer/src/enums/event_type.dart'; +import 'package:smplayer/src/core/player.dart'; +import 'package:smplayer/src/enums/player_state.dart'; +import 'package:smplayer/src/enums/repeat_mode.dart'; +import 'package:smplayer/src/events/duration_change_event.dart'; +import 'package:smplayer/src/events/position_change_event.dart'; +import 'package:smplayer/src/services/isar_service.dart'; +import 'package:smplayer/src/models/previous_playlist_model.dart'; +import 'dart:async'; + +class PlayerChannel { + static const String CHANNEL = 'suamusica.com.br/player'; + static const int ok = 1; + static const int notOk = -1; + + final Player _player; + final MethodChannel _channel; + + PlayerChannel(this._player) : _channel = const MethodChannel(CHANNEL) { + _channel.setMethodCallHandler(platformCallHandler); + } + + Future invokeMethod( + String method, [ + Map? arguments, + ]) async { + try { + final result = await _channel.invokeMethod(method, arguments ?? const {}); + + if (result is int) { + return result; + } + + if (result is bool) { + return result ? ok : notOk; + } + + throw PlatformException( + code: 'INVALID_RESULT_TYPE', + message: 'Expected int or bool result, got ${result.runtimeType}', + ); + } on PlatformException catch (e) { + _log('Platform error in $method: ${e.message}'); + return notOk; + } catch (e) { + _log('Unexpected error in $method: $e'); + return notOk; + } + } + + Future platformCallHandler(MethodCall call) async { + try { + _doHandlePlatformCall(call); + } catch (ex) { + _log('Unexpected error: $ex'); + } + } + + Future _handleOnComplete() async { + _player.state = PlayerState.COMPLETED; + _notifyPlayerStateChangeEvent(EventType.FINISHED_PLAYING, ""); + } + + Future _doHandlePlatformCall(MethodCall call) async { + final currentMedia = _player.currentMedia; + final currentIndex = _player.currentIndex; + final Map callArgs = call.arguments as Map; + if (call.method != 'audio.onCurrentPosition') { + _log('_platformCallHandler call ${call.method} $callArgs'); + } + switch (call.method) { + case 'audio.onDuration': + final duration = callArgs['duration']; + if (duration > 0) { + Duration newDuration = Duration(milliseconds: duration); + _notifyDurationChangeEvent(newDuration); + } + break; + case 'audio.onCurrentPosition': + final position = callArgs['position']; + Duration newPosition = Duration(milliseconds: position); + final duration = callArgs['duration']; + Duration newDuration = Duration(milliseconds: duration); + _notifyPositionChangeEvent(newPosition, newDuration); + break; + case 'audio.onError': + _player.state = PlayerState.ERROR; + final errorType = callArgs['errorType'] ?? 2; + + _notifyPlayerErrorEvent( + error: 'error', + errorType: PlayerErrorType.values[errorType], + ); + break; + case 'state.change': + final state = callArgs['state']; + String error = callArgs['error'] ?? ""; + _log('state.change call ${PlayerState.values[state]}'); + _player.state = PlayerState.values[state]; + switch (_player.state) { + case PlayerState.STATE_READY: + _notifyPlayerStateChangeEvent(EventType.STATE_READY, error); + break; + case PlayerState.IDLE: + _notifyPlayerStateChangeEvent(EventType.IDLE, error); + break; + case PlayerState.BUFFERING: + _notifyPlayerStateChangeEvent(EventType.BUFFERING, error); + break; + case PlayerState.ITEM_TRANSITION: + _notifyPlayerStateChangeEvent(EventType.BEFORE_PLAY, error); + break; + case PlayerState.PLAYING: + _notifyPlayerStateChangeEvent(EventType.PLAYING, error); + break; + case PlayerState.PAUSED: + _notifyPlayerStateChangeEvent(EventType.PAUSED, error); + break; + case PlayerState.STOPPED: + _notifyPlayerStateChangeEvent(EventType.STOP_REQUESTED, error); + break; + case PlayerState.SEEK_END: + _notifyPlayerStateChangeEvent(EventType.SEEK_END, error); + break; + case PlayerState.BUFFER_EMPTY: + _notifyPlayerStateChangeEvent(EventType.BUFFER_EMPTY, error); + break; + case PlayerState.COMPLETED: + _handleOnComplete(); + break; + case PlayerState.STATE_ENDED: + _notifyPlayerStateChangeEvent(EventType.STATE_ENDED, error); + break; + case PlayerState.ERROR: + final error = callArgs['error'] ?? "Unknown from Source"; + final isPermissionError = (error as String).contains( + 'Permission denied', + ); + _notifyPlayerErrorEvent( + error: error, + errorType: isPermissionError + ? PlayerErrorType.PERMISSION_DENIED + : null, + ); + break; + } + break; + case 'commandCenter.onNext': + _log("Player : Command Center : Got a next request"); + await _player.next(); + if (currentMedia != null) { + _player.eventController.add( + Event( + type: EventType.NEXT_NOTIFICATION, + media: currentMedia, + queuePosition: currentIndex, + ), + ); + } + break; + case 'commandCenter.onPrevious': + _log("Player : Command Center : Got a previous request"); + if (currentMedia != null) { + _player.eventController.add( + Event( + type: EventType.PREVIOUS_NOTIFICATION, + media: currentMedia, + queuePosition: currentIndex, + ), + ); + } + _player.previous(); + break; + case 'commandCenter.onPlay': + if (currentMedia != null) { + _player.eventController.add( + Event( + type: EventType.PLAY_NOTIFICATION, + media: currentMedia, + queuePosition: currentIndex, + ), + ); + } + break; + case 'commandCenter.onPause': + if (currentMedia != null) { + _player.eventController.add( + Event( + type: EventType.PAUSED_NOTIFICATION, + media: currentMedia, + queuePosition: currentIndex, + ), + ); + } + break; + case 'commandCenter.onTogglePlayPause': + if (currentMedia != null) { + _player.eventController.add( + Event( + type: EventType.TOGGLE_PLAY_PAUSE, + media: currentMedia, + queuePosition: currentIndex, + ), + ); + } + break; + case 'externalPlayback.play': + print("Player: externalPlayback : Play"); + _notifyPlayerStateChangeEvent(EventType.EXTERNAL_RESUME_REQUESTED, ""); + break; + case 'externalPlayback.pause': + print("Player: externalPlayback : Pause"); + _notifyPlayerStateChangeEvent(EventType.EXTERNAL_PAUSE_REQUESTED, ""); + break; + case 'commandCenter.onFavorite': + final favorite = callArgs['favorite']; + print("Player: onFavorite : $favorite"); + _notifyPlayerStateChangeEvent( + favorite ? EventType.FAVORITE_MUSIC : EventType.UNFAVORITE_MUSIC, + "", + ); + break; + case 'cast.mediaFromQueue': + final index = callArgs['index']; + _channel.invokeMethod('cast_next_media', _player.items[index].toJson()); + _updateQueueIndexAndNotify(index: index); + break; + case 'cast.nextMedia': + case 'cast.previousMedia': + final media = call.method == 'cast.nextMedia' + ? _player.possibleNext(_player.repeatMode) + : _player.possiblePrevious(); + if (media != null) { + _channel.invokeMethod('cast_next_media', media.toJson()); + _updateQueueIndexAndNotify(index: _player.items.indexOf(media)); + } + break; + case 'SET_CURRENT_MEDIA_INDEX': + _updateQueueIndexAndNotify(index: callArgs['CURRENT_MEDIA_INDEX']); + break; + case 'REPEAT_CHANGED': + _player.repeatMode = RepeatMode.values[callArgs['REPEAT_MODE']]; + _notifyPlayerStateChangeEvent(EventType.REPEAT_CHANGED, ""); + break; + case 'SHUFFLE_CHANGED': + _player.shuffleEnabled = callArgs['SHUFFLE_MODE']; + _notifyPlayerStateChangeEvent(EventType.SHUFFLE_CHANGED, ""); + break; + default: + _log('Unknown method ${call.method} '); + } + } + + void _notifyDurationChangeEvent(Duration newDuration) { + final currentIndex = _player.currentIndex; + if (_player.currentMedia != null) { + _player.eventController.add( + DurationChangeEvent( + media: _player.currentMedia!, + queuePosition: currentIndex, + duration: newDuration, + ), + ); + } + } + + void _notifyPlayerStateChangeEvent(EventType eventType, String error) { + final currentIndex = _player.currentIndex; + if (error.isNotEmpty) { + _notifyPlayerErrorEvent( + error: error, + errorType: PlayerErrorType.INFORMATION, + ); + } + if (_player.currentMedia != null) { + _player.eventController.add( + Event( + type: eventType, + media: _player.currentMedia!, + queuePosition: currentIndex, + ), + ); + } + } + + void _notifyPlayerErrorEvent({ + required String error, + PlayerErrorType? errorType, + }) { + final currentIndex = _player.currentIndex; + if (_player.currentMedia != null) { + _player.eventController.add( + Event( + type: EventType.ERROR_OCCURED, + media: _player.currentMedia!, + queuePosition: currentIndex, + error: error, + errorType: errorType ?? PlayerErrorType.UNDEFINED, + ), + ); + } + } + + void _notifyPositionChangeEvent(Duration newPosition, Duration newDuration) { + final media = _player.currentMedia; + final currentIndex = _player.currentIndex; + if (media != null) { + final position = newPosition.inSeconds; + _player.eventController.add( + PositionChangeEvent( + media: media, + queuePosition: currentIndex, + position: newPosition, + duration: newDuration, + ), + ); + if (position >= 0 && position % 5 == 0) { + unawaited( + IsarService.instance.addPreviousPlaylistPosition( + PreviousPlaylistPosition( + mediaId: media.id, + position: newPosition.inMilliseconds.toDouble(), + duration: newDuration.inMilliseconds.toDouble(), + ), + ), + ); + } + } + } + + void _log(String param) { + debugPrint(param); + } + + void _updateQueueIndexAndNotify({required index}) { + _player.setIndexAndUpdateIsar(index); + _notifyPlayerStateChangeEvent(EventType.SET_CURRENT_MEDIA_INDEX, ''); + } +} diff --git a/packages/player/lib/src/core/player_event_controller.dart b/packages/player/lib/src/core/player_event_controller.dart new file mode 100644 index 00000000..e2ad690d --- /dev/null +++ b/packages/player/lib/src/core/player_event_controller.dart @@ -0,0 +1,51 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; + +import 'package:smplayer/src/models/event.dart'; +import 'package:smplayer/src/enums/event_type.dart'; + +class PlayerEventController { + final StreamController _eventStreamController = + StreamController(); + Stream? _stream; + bool _shallSendEvents = true; + final List _chromeCastEnabledEvents = [ + EventType.BEFORE_PLAY, + EventType.NEXT, + EventType.PREVIOUS, + EventType.POSITION_CHANGE, + EventType.REWIND, + EventType.PLAY_REQUESTED, + EventType.PAUSED, + EventType.PLAYING, + EventType.EXTERNAL_RESUME_REQUESTED, + EventType.EXTERNAL_PAUSE_REQUESTED, + EventType.SET_CURRENT_MEDIA_INDEX, + ]; + + Stream get onEvent { + _stream ??= _eventStreamController.stream.asBroadcastStream(); + return _stream!; + } + + bool get shallSendEvents => _shallSendEvents; + set shallSendEvents(bool value) => _shallSendEvents = value; + + void add(Event event) { + if (event.type != EventType.POSITION_CHANGE) { + debugPrint( + 'APP LOGS ==> PlayerEventController _addUsingPlayer ${event.type}', + ); + } + if (!_eventStreamController.isClosed && + (_shallSendEvents || _chromeCastEnabledEvents.contains(event.type))) { + _eventStreamController.add(event); + } + } + + Future dispose() async { + if (!_eventStreamController.isClosed) { + await _eventStreamController.close(); + } + } +} diff --git a/packages/player/lib/src/event_type.dart b/packages/player/lib/src/enums/event_type.dart similarity index 98% rename from packages/player/lib/src/event_type.dart rename to packages/player/lib/src/enums/event_type.dart index ef272629..28716e4e 100644 --- a/packages/player/lib/src/event_type.dart +++ b/packages/player/lib/src/enums/event_type.dart @@ -39,6 +39,7 @@ enum EventType { STATE_ENDED, IDLE, STATE_READY, + CAST_NEXT_MEDIA, } enum PlayerErrorType { diff --git a/packages/player/lib/src/player_state.dart b/packages/player/lib/src/enums/player_state.dart similarity index 100% rename from packages/player/lib/src/player_state.dart rename to packages/player/lib/src/enums/player_state.dart index c3d51628..68750733 100644 --- a/packages/player/lib/src/player_state.dart +++ b/packages/player/lib/src/enums/player_state.dart @@ -9,6 +9,6 @@ enum PlayerState { SEEK_END, BUFFER_EMPTY, ITEM_TRANSITION, - STATE_ENDED, STATE_READY, + STATE_ENDED, } diff --git a/packages/player/lib/src/repeat_mode.dart b/packages/player/lib/src/enums/repeat_mode.dart similarity index 89% rename from packages/player/lib/src/repeat_mode.dart rename to packages/player/lib/src/enums/repeat_mode.dart index da1d5114..e90369fb 100644 --- a/packages/player/lib/src/repeat_mode.dart +++ b/packages/player/lib/src/enums/repeat_mode.dart @@ -9,8 +9,6 @@ extension ParseToString on RepeatMode { return 'Queue'; case RepeatMode.REPEAT_MODE_ONE: return 'Track'; - default: - return "Unknown"; } } } diff --git a/packages/player/lib/src/before_play_event.dart b/packages/player/lib/src/events/before_play_event.dart similarity index 56% rename from packages/player/lib/src/before_play_event.dart rename to packages/player/lib/src/events/before_play_event.dart index 9db10729..2c366f7e 100644 --- a/packages/player/lib/src/before_play_event.dart +++ b/packages/player/lib/src/events/before_play_event.dart @@ -1,6 +1,6 @@ -import 'package:smplayer/src/event.dart'; -import 'package:smplayer/src/event_type.dart'; -import 'package:smplayer/src/media.dart'; +import 'package:smplayer/src/models/event.dart'; +import 'package:smplayer/src/enums/event_type.dart'; +import 'package:smplayer/src/models/media.dart'; class BeforePlayEvent extends Event { Function(bool) operation; @@ -11,10 +11,10 @@ class BeforePlayEvent extends Event { required this.operation, required int queuePosition, }) : super( - type: EventType.BEFORE_PLAY, - media: media, - queuePosition: queuePosition, - ); + type: EventType.BEFORE_PLAY, + media: media, + queuePosition: queuePosition, + ); continueWithLoadingOnly() { this.operation(true); diff --git a/packages/player/lib/src/current_queue_updated.dart b/packages/player/lib/src/events/current_queue_updated.dart similarity index 66% rename from packages/player/lib/src/current_queue_updated.dart rename to packages/player/lib/src/events/current_queue_updated.dart index e093a43e..18c99503 100644 --- a/packages/player/lib/src/current_queue_updated.dart +++ b/packages/player/lib/src/events/current_queue_updated.dart @@ -1,5 +1,5 @@ -import 'package:smplayer/src/event.dart'; -import 'package:smplayer/src/media.dart'; +import 'package:smplayer/src/models/event.dart'; +import 'package:smplayer/src/models/media.dart'; class CurrentQueueUpdated extends Event { CurrentQueueUpdated({ diff --git a/packages/player/lib/src/duration_change_event.dart b/packages/player/lib/src/events/duration_change_event.dart similarity index 52% rename from packages/player/lib/src/duration_change_event.dart rename to packages/player/lib/src/events/duration_change_event.dart index a3ae60e1..3bed8fdb 100644 --- a/packages/player/lib/src/duration_change_event.dart +++ b/packages/player/lib/src/events/duration_change_event.dart @@ -1,6 +1,6 @@ -import 'package:smplayer/src/event.dart'; -import 'package:smplayer/src/event_type.dart'; -import 'package:smplayer/src/media.dart'; +import 'package:smplayer/src/models/event.dart'; +import 'package:smplayer/src/enums/event_type.dart'; +import 'package:smplayer/src/models/media.dart'; class DurationChangeEvent extends Event { final Duration duration; @@ -11,10 +11,10 @@ class DurationChangeEvent extends Event { required this.duration, required queuePosition, }) : super( - type: EventType.DURATION_CHANGE, - media: media, - queuePosition: queuePosition, - ); + type: EventType.DURATION_CHANGE, + media: media, + queuePosition: queuePosition, + ); @override String toString() => "${super.toString()} duration: $duration"; diff --git a/packages/player/lib/src/network_change_event.dart b/packages/player/lib/src/events/network_change_event.dart similarity index 74% rename from packages/player/lib/src/network_change_event.dart rename to packages/player/lib/src/events/network_change_event.dart index 6699cc25..d83c66ac 100644 --- a/packages/player/lib/src/network_change_event.dart +++ b/packages/player/lib/src/events/network_change_event.dart @@ -1,11 +1,8 @@ -import 'package:smplayer/src/event.dart'; -import 'package:smplayer/src/event_type.dart'; -import 'package:smplayer/src/media.dart'; +import 'package:smplayer/src/models/event.dart'; +import 'package:smplayer/src/enums/event_type.dart'; +import 'package:smplayer/src/models/media.dart'; -enum NetworkStatus { - CONNECTED, - DISCONNECTED, -} +enum NetworkStatus { CONNECTED, DISCONNECTED } class NetworkChangeEvent extends Event { NetworkChangeEvent({ @@ -14,10 +11,10 @@ class NetworkChangeEvent extends Event { required int queuePosition, required this.networkStatus, }) : super( - type: EventType.NETWORK_CHANGE, - media: media, - queuePosition: queuePosition, - ); + type: EventType.NETWORK_CHANGE, + media: media, + queuePosition: queuePosition, + ); final NetworkStatus networkStatus; diff --git a/packages/player/lib/src/position_change_event.dart b/packages/player/lib/src/events/position_change_event.dart similarity index 57% rename from packages/player/lib/src/position_change_event.dart rename to packages/player/lib/src/events/position_change_event.dart index bfa7f5b0..11f87aed 100644 --- a/packages/player/lib/src/position_change_event.dart +++ b/packages/player/lib/src/events/position_change_event.dart @@ -1,6 +1,6 @@ -import 'package:smplayer/src/event.dart'; -import 'package:smplayer/src/event_type.dart'; -import 'package:smplayer/src/media.dart'; +import 'package:smplayer/src/models/event.dart'; +import 'package:smplayer/src/enums/event_type.dart'; +import 'package:smplayer/src/models/media.dart'; class PositionChangeEvent extends Event { final Duration position; @@ -12,9 +12,10 @@ class PositionChangeEvent extends Event { required this.position, required this.duration, }) : super( - type: EventType.POSITION_CHANGE, - media: media, - queuePosition: queuePosition); + type: EventType.POSITION_CHANGE, + media: media, + queuePosition: queuePosition, + ); @override String toString() => diff --git a/packages/player/lib/src/event.dart b/packages/player/lib/src/models/event.dart similarity index 89% rename from packages/player/lib/src/event.dart rename to packages/player/lib/src/models/event.dart index 006854a8..2b4797e9 100644 --- a/packages/player/lib/src/event.dart +++ b/packages/player/lib/src/models/event.dart @@ -1,4 +1,6 @@ -import 'package:smplayer/player.dart'; +import 'package:smplayer/src/enums/event_type.dart'; +import 'package:smplayer/src/models/media.dart'; +import 'package:smplayer/src/enums/repeat_mode.dart'; class Event { Event({ diff --git a/packages/player/lib/src/media.dart b/packages/player/lib/src/models/media.dart similarity index 61% rename from packages/player/lib/src/media.dart rename to packages/player/lib/src/models/media.dart index ce6be301..1b61d813 100644 --- a/packages/player/lib/src/media.dart +++ b/packages/player/lib/src/models/media.dart @@ -53,28 +53,28 @@ class Media { } Map toJson() => { - 'id': id, - 'name': name, - 'ownerId': ownerId, - 'albumId': albumId, - 'albumTitle': albumTitle, - 'author': author, - 'url': url, - 'is_local': isLocal, - 'cover_url': coverUrl, - 'bigCover': bigCoverUrl, - 'is_verified': isVerified, - 'shared_url': shareUrl, - 'playlist_id': playlistId, - 'fallbackUrl': fallbackUrl, - 'is_spot': isSpot, - 'isFavorite': isFavorite, - 'indexInPlaylist': indexInPlaylist, - 'catid': categoryId, - 'playlistTitle': playlistTitle, - 'playlistCoverUrl': playlistCoverUrl, - 'playlistOwnerId': playlistOwnerId, - }; + 'id': id, + 'name': name, + 'ownerId': ownerId, + 'albumId': albumId, + 'albumTitle': albumTitle, + 'author': author, + 'url': url, + 'is_local': isLocal, + 'cover_url': coverUrl, + 'bigCover': bigCoverUrl, + 'is_verified': isVerified, + 'shared_url': shareUrl, + 'playlist_id': playlistId, + 'fallbackUrl': fallbackUrl, + 'is_spot': isSpot, + 'isFavorite': isFavorite, + 'indexInPlaylist': indexInPlaylist, + 'catid': categoryId, + 'playlistTitle': playlistTitle, + 'playlistCoverUrl': playlistCoverUrl, + 'playlistOwnerId': playlistOwnerId, + }; @override String toString() => jsonEncode(toJson()); @@ -102,21 +102,21 @@ class Media { @override int get hashCode => Object.hashAll([ - id, - name, - albumId, - albumTitle, - ownerId, - author, - url, - isLocal, - coverUrl, - isVerified, - shareUrl, - playlistId, - isSpot, - isFavorite, - ]); + id, + name, + albumId, + albumTitle, + ownerId, + author, + url, + isLocal, + coverUrl, + isVerified, + shareUrl, + playlistId, + isSpot, + isFavorite, + ]); Media copyWith({ int? id, @@ -141,30 +141,29 @@ class Media { String? playlistTitle, String? playlistCoverUrl, int? playlistOwnerId, - }) => - Media( - id: id ?? this.id, - name: name ?? this.name, - ownerId: ownerId ?? this.ownerId, - albumId: albumId ?? this.albumId, - albumTitle: albumTitle ?? this.albumTitle, - author: author ?? this.author, - url: url ?? this.url, - isLocal: isLocal ?? this.isLocal, - coverUrl: coverUrl ?? this.coverUrl, - bigCoverUrl: bigCoverUrl ?? this.bigCoverUrl, - isVerified: isVerified ?? this.isVerified, - shareUrl: shareUrl ?? this.shareUrl, - playlistId: playlistId ?? this.playlistId, - fallbackUrl: fallbackUrl ?? this.fallbackUrl, - isSpot: isSpot ?? this.isSpot, - isFavorite: isFavorite ?? this.isFavorite, - indexInPlaylist: indexInPlaylist ?? this.indexInPlaylist, - categoryId: categoryId ?? this.categoryId, - playlistTitle: playlistTitle ?? this.playlistTitle, - playlistCoverUrl: playlistCoverUrl ?? this.playlistCoverUrl, - playlistOwnerId: playlistOwnerId ?? this.playlistOwnerId, - ); + }) => Media( + id: id ?? this.id, + name: name ?? this.name, + ownerId: ownerId ?? this.ownerId, + albumId: albumId ?? this.albumId, + albumTitle: albumTitle ?? this.albumTitle, + author: author ?? this.author, + url: url ?? this.url, + isLocal: isLocal ?? this.isLocal, + coverUrl: coverUrl ?? this.coverUrl, + bigCoverUrl: bigCoverUrl ?? this.bigCoverUrl, + isVerified: isVerified ?? this.isVerified, + shareUrl: shareUrl ?? this.shareUrl, + playlistId: playlistId ?? this.playlistId, + fallbackUrl: fallbackUrl ?? this.fallbackUrl, + isSpot: isSpot ?? this.isSpot, + isFavorite: isFavorite ?? this.isFavorite, + indexInPlaylist: indexInPlaylist ?? this.indexInPlaylist, + categoryId: categoryId ?? this.categoryId, + playlistTitle: playlistTitle ?? this.playlistTitle, + playlistCoverUrl: playlistCoverUrl ?? this.playlistCoverUrl, + playlistOwnerId: playlistOwnerId ?? this.playlistOwnerId, + ); factory Media.fromJson(Map map) { return Media( id: map['id']?.toInt() ?? 0, @@ -200,24 +199,19 @@ extension ListMediaToListStringCompressed on List { extension ListStringToListPlayable on List { List get toListMedia => map((e) { - final media = Media.fromJson( - jsonDecode( - e, - ), - ); - return media.copyWith( - coverUrl: media.coverUrl.isEmpty - ? 'https://suamusica.com.br/cover/cd/${media.albumId}' - : media.coverUrl, - bigCoverUrl: media.bigCoverUrl.isEmpty - ? 'https://suamusica.com.br/cover/cd/${media.albumId}' - : media.bigCoverUrl, - author: media.author.isEmpty ? 'Desconhecido' : media.author, - albumTitle: - media.albumTitle.isEmpty ? 'Desconhecido' : media.albumTitle, - name: media.name.isEmpty ? 'Desconhecido' : media.name, - ); - }).toList(); + final media = Media.fromJson(jsonDecode(e)); + return media.copyWith( + coverUrl: media.coverUrl.isEmpty + ? 'https://suamusica.com.br/cover/cd/${media.albumId}' + : media.coverUrl, + bigCoverUrl: media.bigCoverUrl.isEmpty + ? 'https://suamusica.com.br/cover/cd/${media.albumId}' + : media.bigCoverUrl, + author: media.author.isEmpty ? 'Desconhecido' : media.author, + albumTitle: media.albumTitle.isEmpty ? 'Desconhecido' : media.albumTitle, + name: media.name.isEmpty ? 'Desconhecido' : media.name, + ); + }).toList(); } extension CompressRestoreWithGzipB64 on String { diff --git a/packages/player/lib/src/previous_playlist_model.dart b/packages/player/lib/src/models/previous_playlist_model.dart similarity index 93% rename from packages/player/lib/src/previous_playlist_model.dart rename to packages/player/lib/src/models/previous_playlist_model.dart index 34dcdb2b..1407b657 100644 --- a/packages/player/lib/src/previous_playlist_model.dart +++ b/packages/player/lib/src/models/previous_playlist_model.dart @@ -4,10 +4,7 @@ part 'previous_playlist_model.g.dart'; @collection class PreviousPlaylistMusics { - PreviousPlaylistMusics({ - this.id = 1, - this.musics, - }); + PreviousPlaylistMusics({this.id = 1, this.musics}); Id id = Isar.autoIncrement; List? musics; @override diff --git a/packages/player/lib/src/models/previous_playlist_model.g.dart b/packages/player/lib/src/models/previous_playlist_model.g.dart new file mode 100644 index 00000000..ba9e71aa --- /dev/null +++ b/packages/player/lib/src/models/previous_playlist_model.g.dart @@ -0,0 +1,2053 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'previous_playlist_model.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetPreviousPlaylistMusicsCollection on Isar { + IsarCollection get previousPlaylistMusics => + this.collection(); +} + +const PreviousPlaylistMusicsSchema = CollectionSchema( + name: r'PreviousPlaylistMusics', + id: 1445680560601406369, + properties: { + r'musics': PropertySchema( + id: 0, + name: r'musics', + type: IsarType.stringList, + ), + }, + estimateSize: _previousPlaylistMusicsEstimateSize, + serialize: _previousPlaylistMusicsSerialize, + deserialize: _previousPlaylistMusicsDeserialize, + deserializeProp: _previousPlaylistMusicsDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _previousPlaylistMusicsGetId, + getLinks: _previousPlaylistMusicsGetLinks, + attach: _previousPlaylistMusicsAttach, + version: '3.1.0', +); + +int _previousPlaylistMusicsEstimateSize( + PreviousPlaylistMusics object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final list = object.musics; + if (list != null) { + bytesCount += 3 + list.length * 3; + { + for (var i = 0; i < list.length; i++) { + final value = list[i]; + bytesCount += value.length * 3; + } + } + } + } + return bytesCount; +} + +void _previousPlaylistMusicsSerialize( + PreviousPlaylistMusics object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeStringList(offsets[0], object.musics); +} + +PreviousPlaylistMusics _previousPlaylistMusicsDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = PreviousPlaylistMusics( + id: id, + musics: reader.readStringList(offsets[0]), + ); + return object; +} + +P _previousPlaylistMusicsDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readStringList(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _previousPlaylistMusicsGetId(PreviousPlaylistMusics object) { + return object.id; +} + +List> _previousPlaylistMusicsGetLinks( + PreviousPlaylistMusics object, +) { + return []; +} + +void _previousPlaylistMusicsAttach( + IsarCollection col, + Id id, + PreviousPlaylistMusics object, +) { + object.id = id; +} + +extension PreviousPlaylistMusicsQueryWhereSort + on QueryBuilder { + QueryBuilder + anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension PreviousPlaylistMusicsQueryWhere + on + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QWhereClause + > { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterWhereClause + > + idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterWhereClause + > + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterWhereClause + > + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterWhereClause + > + idLessThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterWhereClause + > + idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + ), + ); + }); + } +} + +extension PreviousPlaylistMusicsQueryFilter + on + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QFilterCondition + > { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + idGreaterThan(Id value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + idLessThan(Id value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'musics'), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'musics'), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementEqualTo(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'musics', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementEndsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'musics', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'musics', value: ''), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'musics', value: ''), + ); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength(r'musics', length, true, length, true); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength(r'musics', 0, true, 0, true); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength(r'musics', 0, false, 999999, true); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsLengthLessThan(int length, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.listLength(r'musics', 0, true, length, include); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsLengthGreaterThan(int length, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.listLength(r'musics', length, include, 999999, true); + }); + } + + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'musics', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } +} + +extension PreviousPlaylistMusicsQueryObject + on + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QFilterCondition + > {} + +extension PreviousPlaylistMusicsQueryLinks + on + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QFilterCondition + > {} + +extension PreviousPlaylistMusicsQuerySortBy + on QueryBuilder {} + +extension PreviousPlaylistMusicsQuerySortThenBy + on + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QSortThenBy + > { + QueryBuilder + thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } +} + +extension PreviousPlaylistMusicsQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByMusics() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'musics'); + }); + } +} + +extension PreviousPlaylistMusicsQueryProperty + on + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QQueryProperty + > { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder?, QQueryOperations> + musicsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'musics'); + }); + } +} + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetPreviousPlaylistCurrentIndexCollection on Isar { + IsarCollection + get previousPlaylistCurrentIndexs => this.collection(); +} + +const PreviousPlaylistCurrentIndexSchema = CollectionSchema( + name: r'PreviousPlaylistCurrentIndex', + id: -9036091697598573141, + properties: { + r'currentIndex': PropertySchema( + id: 0, + name: r'currentIndex', + type: IsarType.long, + ), + r'mediaId': PropertySchema(id: 1, name: r'mediaId', type: IsarType.long), + }, + estimateSize: _previousPlaylistCurrentIndexEstimateSize, + serialize: _previousPlaylistCurrentIndexSerialize, + deserialize: _previousPlaylistCurrentIndexDeserialize, + deserializeProp: _previousPlaylistCurrentIndexDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _previousPlaylistCurrentIndexGetId, + getLinks: _previousPlaylistCurrentIndexGetLinks, + attach: _previousPlaylistCurrentIndexAttach, + version: '3.1.0', +); + +int _previousPlaylistCurrentIndexEstimateSize( + PreviousPlaylistCurrentIndex object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + return bytesCount; +} + +void _previousPlaylistCurrentIndexSerialize( + PreviousPlaylistCurrentIndex object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeLong(offsets[0], object.currentIndex); + writer.writeLong(offsets[1], object.mediaId); +} + +PreviousPlaylistCurrentIndex _previousPlaylistCurrentIndexDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = PreviousPlaylistCurrentIndex( + currentIndex: reader.readLongOrNull(offsets[0]), + id: id, + mediaId: reader.readLong(offsets[1]), + ); + return object; +} + +P _previousPlaylistCurrentIndexDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readLongOrNull(offset)) as P; + case 1: + return (reader.readLong(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _previousPlaylistCurrentIndexGetId(PreviousPlaylistCurrentIndex object) { + return object.id; +} + +List> _previousPlaylistCurrentIndexGetLinks( + PreviousPlaylistCurrentIndex object, +) { + return []; +} + +void _previousPlaylistCurrentIndexAttach( + IsarCollection col, + Id id, + PreviousPlaylistCurrentIndex object, +) { + object.id = id; +} + +extension PreviousPlaylistCurrentIndexQueryWhereSort + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QWhere + > { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterWhere + > + anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension PreviousPlaylistCurrentIndexQueryWhere + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QWhereClause + > { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterWhereClause + > + idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterWhereClause + > + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterWhereClause + > + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterWhereClause + > + idLessThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterWhereClause + > + idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + ), + ); + }); + } +} + +extension PreviousPlaylistCurrentIndexQueryFilter + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QFilterCondition + > { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + currentIndexIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'currentIndex'), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + currentIndexIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'currentIndex'), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + currentIndexEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'currentIndex', value: value), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + currentIndexGreaterThan(int? value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'currentIndex', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + currentIndexLessThan(int? value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'currentIndex', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + currentIndexBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'currentIndex', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + idGreaterThan(Id value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + idLessThan(Id value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + mediaIdEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'mediaId', value: value), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + mediaIdGreaterThan(int value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'mediaId', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + mediaIdLessThan(int value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'mediaId', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + mediaIdBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'mediaId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); + }); + } +} + +extension PreviousPlaylistCurrentIndexQueryObject + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QFilterCondition + > {} + +extension PreviousPlaylistCurrentIndexQueryLinks + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QFilterCondition + > {} + +extension PreviousPlaylistCurrentIndexQuerySortBy + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QSortBy + > { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + sortByCurrentIndex() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'currentIndex', Sort.asc); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + sortByCurrentIndexDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'currentIndex', Sort.desc); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + sortByMediaId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mediaId', Sort.asc); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + sortByMediaIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mediaId', Sort.desc); + }); + } +} + +extension PreviousPlaylistCurrentIndexQuerySortThenBy + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QSortThenBy + > { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + thenByCurrentIndex() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'currentIndex', Sort.asc); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + thenByCurrentIndexDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'currentIndex', Sort.desc); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + thenByMediaId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mediaId', Sort.asc); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + thenByMediaIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mediaId', Sort.desc); + }); + } +} + +extension PreviousPlaylistCurrentIndexQueryWhereDistinct + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QDistinct + > { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QDistinct + > + distinctByCurrentIndex() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'currentIndex'); + }); + } + + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QDistinct + > + distinctByMediaId() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'mediaId'); + }); + } +} + +extension PreviousPlaylistCurrentIndexQueryProperty + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QQueryProperty + > { + QueryBuilder + idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder + currentIndexProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'currentIndex'); + }); + } + + QueryBuilder + mediaIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'mediaId'); + }); + } +} + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetPreviousPlaylistPositionCollection on Isar { + IsarCollection get previousPlaylistPositions => + this.collection(); +} + +const PreviousPlaylistPositionSchema = CollectionSchema( + name: r'PreviousPlaylistPosition', + id: 4468986416954213317, + properties: { + r'duration': PropertySchema( + id: 0, + name: r'duration', + type: IsarType.double, + ), + r'mediaId': PropertySchema(id: 1, name: r'mediaId', type: IsarType.long), + r'position': PropertySchema( + id: 2, + name: r'position', + type: IsarType.double, + ), + }, + estimateSize: _previousPlaylistPositionEstimateSize, + serialize: _previousPlaylistPositionSerialize, + deserialize: _previousPlaylistPositionDeserialize, + deserializeProp: _previousPlaylistPositionDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _previousPlaylistPositionGetId, + getLinks: _previousPlaylistPositionGetLinks, + attach: _previousPlaylistPositionAttach, + version: '3.1.0', +); + +int _previousPlaylistPositionEstimateSize( + PreviousPlaylistPosition object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + return bytesCount; +} + +void _previousPlaylistPositionSerialize( + PreviousPlaylistPosition object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeDouble(offsets[0], object.duration); + writer.writeLong(offsets[1], object.mediaId); + writer.writeDouble(offsets[2], object.position); +} + +PreviousPlaylistPosition _previousPlaylistPositionDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = PreviousPlaylistPosition( + duration: reader.readDouble(offsets[0]), + id: id, + mediaId: reader.readLong(offsets[1]), + position: reader.readDouble(offsets[2]), + ); + return object; +} + +P _previousPlaylistPositionDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readDouble(offset)) as P; + case 1: + return (reader.readLong(offset)) as P; + case 2: + return (reader.readDouble(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _previousPlaylistPositionGetId(PreviousPlaylistPosition object) { + return object.id; +} + +List> _previousPlaylistPositionGetLinks( + PreviousPlaylistPosition object, +) { + return []; +} + +void _previousPlaylistPositionAttach( + IsarCollection col, + Id id, + PreviousPlaylistPosition object, +) { + object.id = id; +} + +extension PreviousPlaylistPositionQueryWhereSort + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QWhere + > { + QueryBuilder + anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension PreviousPlaylistPositionQueryWhere + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QWhereClause + > { + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterWhereClause + > + idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterWhereClause + > + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterWhereClause + > + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterWhereClause + > + idLessThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterWhereClause + > + idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + ), + ); + }); + } +} + +extension PreviousPlaylistPositionQueryFilter + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QFilterCondition + > { + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + durationEqualTo(double value, {double epsilon = Query.epsilon}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'duration', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + durationGreaterThan( + double value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'duration', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + durationLessThan( + double value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'duration', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + durationBetween( + double lower, + double upper, { + bool includeLower = true, + bool includeUpper = true, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'duration', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + idGreaterThan(Id value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + idLessThan(Id value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + mediaIdEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'mediaId', value: value), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + mediaIdGreaterThan(int value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'mediaId', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + mediaIdLessThan(int value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'mediaId', + value: value, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + mediaIdBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'mediaId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + positionEqualTo(double value, {double epsilon = Query.epsilon}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'position', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + positionGreaterThan( + double value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'position', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + positionLessThan( + double value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'position', + value: value, + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + positionBetween( + double lower, + double upper, { + bool includeLower = true, + bool includeUpper = true, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'position', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + epsilon: epsilon, + ), + ); + }); + } +} + +extension PreviousPlaylistPositionQueryObject + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QFilterCondition + > {} + +extension PreviousPlaylistPositionQueryLinks + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QFilterCondition + > {} + +extension PreviousPlaylistPositionQuerySortBy + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QSortBy + > { + QueryBuilder + sortByDuration() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'duration', Sort.asc); + }); + } + + QueryBuilder + sortByDurationDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'duration', Sort.desc); + }); + } + + QueryBuilder + sortByMediaId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mediaId', Sort.asc); + }); + } + + QueryBuilder + sortByMediaIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mediaId', Sort.desc); + }); + } + + QueryBuilder + sortByPosition() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'position', Sort.asc); + }); + } + + QueryBuilder + sortByPositionDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'position', Sort.desc); + }); + } +} + +extension PreviousPlaylistPositionQuerySortThenBy + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QSortThenBy + > { + QueryBuilder + thenByDuration() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'duration', Sort.asc); + }); + } + + QueryBuilder + thenByDurationDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'duration', Sort.desc); + }); + } + + QueryBuilder + thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenByMediaId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mediaId', Sort.asc); + }); + } + + QueryBuilder + thenByMediaIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mediaId', Sort.desc); + }); + } + + QueryBuilder + thenByPosition() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'position', Sort.asc); + }); + } + + QueryBuilder + thenByPositionDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'position', Sort.desc); + }); + } +} + +extension PreviousPlaylistPositionQueryWhereDistinct + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QDistinct + > { + QueryBuilder + distinctByDuration() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'duration'); + }); + } + + QueryBuilder + distinctByMediaId() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'mediaId'); + }); + } + + QueryBuilder + distinctByPosition() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'position'); + }); + } +} + +extension PreviousPlaylistPositionQueryProperty + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QQueryProperty + > { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder + durationProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'duration'); + }); + } + + QueryBuilder + mediaIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'mediaId'); + }); + } + + QueryBuilder + positionProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'position'); + }); + } +} diff --git a/packages/player/lib/src/queue_item.dart b/packages/player/lib/src/models/queue_item.dart similarity index 89% rename from packages/player/lib/src/queue_item.dart rename to packages/player/lib/src/models/queue_item.dart index 7647a178..83540a6a 100644 --- a/packages/player/lib/src/queue_item.dart +++ b/packages/player/lib/src/models/queue_item.dart @@ -16,11 +16,7 @@ class QueueItem { @override int get hashCode => [originalPosition, position, item].hashCode; - QueueItem copyWith({ - int? originalPosition, - int? position, - T? item, - }) { + QueueItem copyWith({int? originalPosition, int? position, T? item}) { return QueueItem( originalPosition ?? this.originalPosition, position ?? this.position, diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart deleted file mode 100644 index e8738172..00000000 --- a/packages/player/lib/src/player.dart +++ /dev/null @@ -1,857 +0,0 @@ -import 'dart:async'; -import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:smaws/aws.dart'; -import 'package:flutter/services.dart'; -import 'package:smplayer/src/event.dart'; -import 'package:smplayer/src/event_type.dart'; -import 'package:smplayer/src/isar_service.dart'; -import 'package:smplayer/src/media.dart'; -import 'package:smplayer/src/duration_change_event.dart'; -import 'package:smplayer/src/position_change_event.dart'; -import 'package:smplayer/src/previous_playlist_model.dart'; -import 'package:smplayer/src/queue.dart'; -import 'package:smplayer/src/repeat_mode.dart'; -import 'package:mutex/mutex.dart'; - -import 'player_state.dart'; - -class Player { - Player({ - required this.playerId, - required this.cookieSigner, - required this.localMediaValidator, - this.initializeIsar = false, - this.autoPlay = false, - }) { - _queue = Queue( - beforeInitialize: () async => await _channel.invokeMethod('remove_all'), - initializeIsar: this.initializeIsar, - onInitialize: () async { - await enqueueAll( - items, - alreadyAddedToStorage: true, - shouldNotifyTransition: false, - ); - }, - ); - player = this; - } - static const Ok = 1; - static const NotOk = -1; - static const CHANNEL = 'suamusica.com.br/player'; - static final MethodChannel _channel = const MethodChannel(CHANNEL) - ..setMethodCallHandler(platformCallHandler); - - static late Player player; - static bool logEnabled = false; - - bool _shallSendEvents = true; - bool initializeIsar; - bool externalPlayback = false; - bool get itemsReady => _queue.itemsReady; - - CookiesForCustomPolicy? _cookies; - PlayerState state = PlayerState.IDLE; - static late Queue _queue; - static RepeatMode _repeatMode = RepeatMode.REPEAT_MODE_OFF; - static bool _shuffleEnabled = false; - static int _idSum = 0; - final mutex = Mutex(); - - final String playerId; - - int get idSum => _idSum; - set idSum(int value) => _idSum = value; - bool get isShuffleEnabled => _shuffleEnabled; - RepeatMode get repeatMode => _repeatMode; - - set setShuffleEnabled(bool value) => _shuffleEnabled = value; - set repeatMode(RepeatMode value) => _repeatMode = value; - - final StreamController _eventStreamController = - StreamController(); - - final Future Function() cookieSigner; - final String? Function(Media)? localMediaValidator; - final bool autoPlay; - final chromeCastEnabledEvents = [ - EventType.BEFORE_PLAY, - EventType.NEXT, - EventType.PREVIOUS, - EventType.POSITION_CHANGE, - EventType.REWIND, - EventType.PLAY_REQUESTED, - EventType.PAUSED, - EventType.PLAYING, - EventType.EXTERNAL_RESUME_REQUESTED, - EventType.EXTERNAL_PAUSE_REQUESTED, - EventType.SET_CURRENT_MEDIA_INDEX - ]; - - Stream? _stream; - - Stream get onEvent { - _stream ??= _eventStreamController.stream.asBroadcastStream(); - return _stream!; - } - - Future _invokeMethod( - String method, [ - Map? arguments, - ]) async { - if (!_shallSendEvents) { - return NotOk; - } - arguments ??= const {}; - final Map args = Map.of(arguments) - ..['playerId'] = playerId - ..['shallSendEvents'] = _shallSendEvents - ..['externalplayback'] = externalPlayback; - - return _channel - .invokeMethod(method, args) - .then((result) => result ?? Future.value(Ok)); - } - - set setQueuePosition(int position) { - _queue.setIndex = position; - } - - Future updateMediaUri({required int id, String? uri}) async { - _channel.invokeMethod('update_media_uri', { - 'id': id, - 'uri': uri, - }); - return Ok; - } - - Future removeNotification() async { - await _channel.invokeMethod('remove_notification'); - return Ok; - } - - Future enqueueAll( - List items, { - bool autoPlay = false, - bool saveOnTop = false, - bool alreadyAddedToStorage = false, - bool shouldNotifyTransition = true, - }) async { - if (!alreadyAddedToStorage) { - _queue.addAll(items, saveOnTop: saveOnTop); - } - 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: localPath ?? media.url, - ) - .toJson(), - }; - }, - ).toList(); - for (int i = 0; i < batchArgs.length; i += batchSize) { - final batch = batchArgs.sublist(i, min(i + batchSize, batchArgs.length)); - unawaited( - _channel.invokeMethod( - 'enqueue', - { - 'batch': batch, - 'autoPlay': autoPlay, - 'playerId': playerId, - 'shallSendEvents': _shallSendEvents, - 'externalplayback': externalPlayback, - 'shouldNotifyTransition': - batch.length > 1 ? false : shouldNotifyTransition, - if (i == 0) ...{ - 'cookie': cookie, - }, - }, - ), - ); - } - - 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, - }) { - _channel.invokeMethod('remove_in', {'indexesToDelete': positionsToDelete}); - - return _queue.removeByPosition( - positionsToDelete: positionsToDelete, - isShuffle: isShuffleEnabled, - ); - } - - Future removeAll() async { - _queue.clear(); - setQueuePosition = 0; - await IsarService.instance.removeAllMusics(); - _channel.invokeMethod('remove_all'); - return Ok; - } - - Future adsPlaying() async { - await _invokeMethod('ads_playing'); - return Ok; - } - - int enableEvents() { - this._shallSendEvents = true; - return Ok; - } - - int disableEvents() { - this._shallSendEvents = false; - return Ok; - } - - Future restartQueue() async { - final media = _queue.restart(); - return media; - } - - Future reorder( - int oldIndex, - int newIndex, - ) async { - _queue.reorder(oldIndex, newIndex, isShuffleEnabled); - debugPrint('#_queue.reorder: ${getPositionsList()}'); - _channel.invokeMethod('reorder', { - 'oldIndex': oldIndex, - 'newIndex': newIndex, - 'positionsList': getPositionsList(), - }); - return Ok; - } - - Future clear() async => removeAll(); - - Media? get currentMedia => _queue.current; - - int get previousPlaylistIndex => _queue.previousIndex; - PreviousPlaylistPosition? get previousPlaylistPosition => - _queue.previousPosition; - - List get items => _queue.items; - int get size => items.length; - - int get currentIndex => _queue.index; - - Future play({bool shouldPrepare = false}) async { - await _invokeMethod( - 'play', - { - 'shouldPrepare': shouldPrepare, - }, - ); - 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, - bool loadOnly = false, - }) async { - if (!loadOnly) { - _notifyPlayerStatusChangeEvent(EventType.PLAY_REQUESTED); - } - if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { - setRepeatMode("all"); - } - return _channel.invokeMethod('playFromQueue', { - 'position': pos, - 'timePosition': position?.inMilliseconds, - 'loadOnly': loadOnly, - }).then((result) => result); - } - - List> getPositionsList() { - return [ - for (var item in _queue.storage) - { - 'originalPosition': item.originalPosition, - } - ]; - } - - Future forward() async { - if (currentMedia == null) { - return NotOk; - } - return _forward(currentMedia); - } - - Future _forward(Media? media) async { - if (media == null) { - return NotOk; - } - final duration = Duration(milliseconds: await getDuration()); - _notifyPositionChangeEvent(this, duration, duration); - _notifyForward(media); - return stop(); - } - - Future toggleRepeatMode() async { - 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') - .then((result) => result); - } - - 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) { - return _queue.items.indexOf(media); - } - if (media == null) { - return null; - } - if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { - setRepeatMode("all"); - } - return await _invokeMethod('previous'); - } - - Future next({bool isFromChromecast = false}) async { - final media = _queue.possibleNext(repeatMode); - if (isFromChromecast && media != null) { - return _queue.items.indexOf(media); - } - if (media != null) { - if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { - setRepeatMode("all"); - } - return _invokeMethod('next'); - } else { - return null; - } - } - - Future updateNotification({ - required bool isFavorite, - required int id, - }) async { - return _channel.invokeMethod('update_notification', { - 'isFavorite': isFavorite, - 'idFavorite': id, - }).then((result) => result); - } - - Future pause() async { - _notifyPlayerStatusChangeEvent(EventType.PAUSE_REQUEST); - return await _invokeMethod('pause'); - } - - void addUsingPlayer(Event event) => _addUsingPlayer(player, event); - - Future stop() async { - // _notifyPlayerStatusChangeEvent(EventType.STOP_REQUESTED); - // final int result = await _invokeMethod('stop'); - - // if (result == Ok) { - // state = PlayerState.STOPPED; - // _notifyPlayerStatusChangeEvent(EventType.STOPPED); - // } - - // return result; - return Ok; - } - - Future release() async { - _notifyPlayerStatusChangeEvent(EventType.RELEASE_REQUESTED); - final int result = await _invokeMethod('release'); - - if (result == Ok) { - state = PlayerState.STOPPED; - _notifyPlayerStatusChangeEvent(EventType.RELEASED); - } - _queue.dispose(); - return result; - } - - Future toggleShuffle() async { - if (!isShuffleEnabled) { - _queue.shuffle(); - } else { - _queue.unshuffle(); - } - debugPrint('#_queue.shuffle: ${getPositionsList()}'); - _channel - .invokeMethod('toggle_shuffle', {'positionsList': getPositionsList()}); - } - - Future seek(Duration position) { - _notifyPlayerStateChangeEvent(this, EventType.SEEK_START, ""); - return _invokeMethod('seek', {'position': position.inMilliseconds}); - } - - Future setVolume(double volume) { - return _invokeMethod('setVolume', {'volume': volume}); - } - - Future getDuration() { - return _invokeMethod('getDuration'); - } - - Future getCurrentPosition() async { - return _invokeMethod('getCurrentPosition'); - } - - static Future platformCallHandler(MethodCall call) async { - try { - _doHandlePlatformCall(call); - } catch (ex) { - _log('Unexpected error: $ex'); - } - } - - 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'); - } - switch (call.method) { - case 'audio.onDuration': - final duration = callArgs['duration']; - if (duration > 0) { - Duration newDuration = Duration(milliseconds: duration); - _notifyDurationChangeEvent(player, newDuration); - } - break; - case 'audio.onCurrentPosition': - final position = callArgs['position']; - Duration newPosition = Duration(milliseconds: position); - final duration = callArgs['duration']; - Duration newDuration = Duration(milliseconds: duration); - _notifyPositionChangeEvent(player, newPosition, newDuration); - break; - case 'audio.onError': - player.state = PlayerState.ERROR; - final errorType = callArgs['errorType'] ?? 2; - - _notifyPlayerErrorEvent( - player: player, - error: 'error', - errorType: PlayerErrorType.values[errorType], - ); - break; - case 'state.change': - final state = callArgs['state']; - String error = callArgs['error'] ?? ""; - _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( - player, - EventType.BUFFERING, - error, - ); - break; - case PlayerState.ITEM_TRANSITION: - _notifyPlayerStateChangeEvent( - player, - EventType.BEFORE_PLAY, - error, - ); - break; - case PlayerState.PLAYING: - _notifyPlayerStateChangeEvent( - player, - EventType.PLAYING, - error, - ); - break; - case PlayerState.PAUSED: - _notifyPlayerStateChangeEvent( - player, - EventType.PAUSED, - error, - ); - break; - - case PlayerState.STOPPED: - _notifyPlayerStateChangeEvent( - player, - EventType.STOP_REQUESTED, - error, - ); - break; - - case PlayerState.SEEK_END: - _notifyPlayerStateChangeEvent( - player, - EventType.SEEK_END, - error, - ); - break; - - case PlayerState.BUFFER_EMPTY: - _notifyPlayerStateChangeEvent( - player, - EventType.BUFFER_EMPTY, - error, - ); - break; - - case PlayerState.COMPLETED: - // _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 = - (error as String).contains('Permission denied'); - _notifyPlayerErrorEvent( - player: player, - error: error, - errorType: isPermissionError - ? PlayerErrorType.PERMISSION_DENIED - : null); - break; - } - - break; - case 'commandCenter.onNext': - _log("Player : Command Center : Got a next request"); - await player.next(); - if (currentMedia != null) { - _addUsingPlayer( - player, - Event( - type: EventType.NEXT_NOTIFICATION, - media: currentMedia, - queuePosition: currentIndex, - ), - ); - } - break; - case 'commandCenter.onPrevious': - _log("Player : Command Center : Got a previous request"); - if (currentMedia != null) { - _addUsingPlayer( - player, - Event( - type: EventType.PREVIOUS_NOTIFICATION, - media: currentMedia, - queuePosition: currentIndex, - ), - ); - } - player.previous(); - break; - case 'commandCenter.onPlay': - if (currentMedia != null) { - _addUsingPlayer( - player, - Event( - type: EventType.PLAY_NOTIFICATION, - media: currentMedia, - queuePosition: currentIndex, - ), - ); - } - break; - case 'commandCenter.onPause': - if (currentMedia != null) { - _addUsingPlayer( - player, - Event( - type: EventType.PAUSED_NOTIFICATION, - media: currentMedia, - queuePosition: currentIndex, - ), - ); - } - break; - case 'commandCenter.onTogglePlayPause': - if (currentMedia != null) { - _addUsingPlayer( - player, - Event( - type: EventType.TOGGLE_PLAY_PAUSE, - media: currentMedia, - queuePosition: currentIndex, - ), - ); - } - break; - case 'externalPlayback.play': - print("Player: externalPlayback : Play"); - _notifyPlayerStateChangeEvent( - player, EventType.EXTERNAL_RESUME_REQUESTED, ""); - break; - case 'externalPlayback.pause': - print("Player: externalPlayback : Pause"); - _notifyPlayerStateChangeEvent( - player, - EventType.EXTERNAL_PAUSE_REQUESTED, - "", - ); - break; - case 'commandCenter.onFavorite': - final favorite = callArgs['favorite']; - print("Player: onFavorite : $favorite"); - _notifyPlayerStateChangeEvent( - player, - favorite ? EventType.FAVORITE_MUSIC : EventType.UNFAVORITE_MUSIC, - "", - ); - - break; - case 'SET_CURRENT_MEDIA_INDEX': - _queue.setIndex = callArgs['CURRENT_MEDIA_INDEX']; - _queue.updateIsarIndex(currentMedia!.id, _queue.index); - _notifyPlayerStateChangeEvent( - player, - EventType.SET_CURRENT_MEDIA_INDEX, - "", - ); - break; - case 'REPEAT_CHANGED': - _repeatMode = RepeatMode.values[callArgs['REPEAT_MODE']]; - _notifyPlayerStateChangeEvent( - player, - EventType.REPEAT_CHANGED, - "", - ); - break; - case 'SHUFFLE_CHANGED': - _shuffleEnabled = callArgs['SHUFFLE_MODE']; - _notifyPlayerStateChangeEvent( - player, - EventType.SHUFFLE_CHANGED, - "", - ); - break; - default: - _log('Unknown method ${call.method} '); - } - } - - // _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(); - final durationInMilli = await getDuration(); - - _add(Event( - type: EventType.FORWARD, - media: media, - queuePosition: currentIndex, - position: Duration(milliseconds: positionInMilli), - duration: Duration(milliseconds: durationInMilli), - )); - } - - _notifyPlayerStatusChangeEvent(EventType type) { - if (currentMedia != null) { - _add( - Event( - type: type, - media: currentMedia!, - queuePosition: currentIndex, - ), - ); - } - } - - static _notifyDurationChangeEvent(Player player, Duration newDuration) { - final currentIndex = _queue.index; - if (_queue.current != null) { - _addUsingPlayer( - player, - DurationChangeEvent( - media: _queue.current!, - queuePosition: currentIndex, - duration: newDuration)); - } - } - - static _notifyPlayerStateChangeEvent( - Player player, - EventType eventType, - String error, - ) { - final currentIndex = _queue.index; - if (error.isNotEmpty) { - _notifyPlayerErrorEvent( - player: player, - error: error, - errorType: PlayerErrorType.INFORMATION, - ); - } - if (_queue.current != null) { - _addUsingPlayer( - player, - Event( - type: eventType, - media: _queue.current!, - queuePosition: currentIndex, - ), - ); - } - } - - static _notifyPlayerErrorEvent({ - required Player player, - required String error, - PlayerErrorType? errorType, - }) { - final currentIndex = _queue.index; - if (_queue.current != null) { - _addUsingPlayer( - player, - Event( - type: EventType.ERROR_OCCURED, - media: _queue.current!, - queuePosition: currentIndex, - error: error, - errorType: errorType ?? PlayerErrorType.UNDEFINED, - ), - ); - } - } - - static _notifyPositionChangeEvent( - Player player, Duration newPosition, Duration newDuration) { - final media = _queue.current; - final currentIndex = _queue.index; - if (media != null) { - final position = newPosition.inSeconds; - _addUsingPlayer( - player, - PositionChangeEvent( - media: media, - queuePosition: currentIndex, - position: newPosition, - duration: newDuration, - ), - ); - if (position >= 0 && position % 5 == 0) { - unawaited( - IsarService.instance.addPreviousPlaylistPosition( - PreviousPlaylistPosition( - mediaId: media.id, - position: newPosition.inMilliseconds.toDouble(), - duration: newDuration.inMilliseconds.toDouble(), - ), - ), - ); - } - } - } - - static void _log(String param) { - debugPrint(param); - } - - void _add(Event event) { - if (!_eventStreamController.isClosed && - (_shallSendEvents || chromeCastEnabledEvents.contains(event.type))) { - _eventStreamController.add(event); - } - } - - static void _addUsingPlayer(Player player, Event event) { - if (event.type != EventType.POSITION_CHANGE) { - debugPrint("_platformCallHandler _addUsingPlayer $event"); - } - if (!player._eventStreamController.isClosed && - (player._shallSendEvents || - player.chromeCastEnabledEvents.contains(event.type))) { - player._eventStreamController.add(event); - } - } - - Future dispose() async { - List futures = []; - if (!_eventStreamController.isClosed) { - futures.add(_eventStreamController.close()); - } - await Future.wait(futures); - } -} diff --git a/packages/player/lib/src/previous_playlist_model.g.dart b/packages/player/lib/src/previous_playlist_model.g.dart deleted file mode 100644 index f2380e7c..00000000 --- a/packages/player/lib/src/previous_playlist_model.g.dart +++ /dev/null @@ -1,1649 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'previous_playlist_model.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetPreviousPlaylistMusicsCollection on Isar { - IsarCollection get previousPlaylistMusics => - this.collection(); -} - -const PreviousPlaylistMusicsSchema = CollectionSchema( - name: r'PreviousPlaylistMusics', - id: 1445680560601406369, - properties: { - r'musics': PropertySchema( - id: 0, - name: r'musics', - type: IsarType.stringList, - ) - }, - estimateSize: _previousPlaylistMusicsEstimateSize, - serialize: _previousPlaylistMusicsSerialize, - deserialize: _previousPlaylistMusicsDeserialize, - deserializeProp: _previousPlaylistMusicsDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - getId: _previousPlaylistMusicsGetId, - getLinks: _previousPlaylistMusicsGetLinks, - attach: _previousPlaylistMusicsAttach, - version: '3.1.0', -); - -int _previousPlaylistMusicsEstimateSize( - PreviousPlaylistMusics object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - { - final list = object.musics; - if (list != null) { - bytesCount += 3 + list.length * 3; - { - for (var i = 0; i < list.length; i++) { - final value = list[i]; - bytesCount += value.length * 3; - } - } - } - } - return bytesCount; -} - -void _previousPlaylistMusicsSerialize( - PreviousPlaylistMusics object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeStringList(offsets[0], object.musics); -} - -PreviousPlaylistMusics _previousPlaylistMusicsDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = PreviousPlaylistMusics( - id: id, - musics: reader.readStringList(offsets[0]), - ); - return object; -} - -P _previousPlaylistMusicsDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readStringList(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _previousPlaylistMusicsGetId(PreviousPlaylistMusics object) { - return object.id; -} - -List> _previousPlaylistMusicsGetLinks( - PreviousPlaylistMusics object) { - return []; -} - -void _previousPlaylistMusicsAttach( - IsarCollection col, Id id, PreviousPlaylistMusics object) { - object.id = id; -} - -extension PreviousPlaylistMusicsQueryWhereSort - on QueryBuilder { - QueryBuilder - anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension PreviousPlaylistMusicsQueryWhere on QueryBuilder< - PreviousPlaylistMusics, PreviousPlaylistMusics, QWhereClause> { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); - }); - } -} - -extension PreviousPlaylistMusicsQueryFilter on QueryBuilder< - PreviousPlaylistMusics, PreviousPlaylistMusics, QFilterCondition> { - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder musicsIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'musics', - )); - }); - } - - QueryBuilder musicsIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'musics', - )); - }); - } - - QueryBuilder musicsElementEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'musics', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder musicsElementGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'musics', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder musicsElementLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'musics', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder musicsElementBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'musics', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder musicsElementStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'musics', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder musicsElementEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'musics', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - musicsElementContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'musics', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - musicsElementMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'musics', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder musicsElementIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'musics', - value: '', - )); - }); - } - - QueryBuilder musicsElementIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'musics', - value: '', - )); - }); - } - - QueryBuilder musicsLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - length, - true, - length, - true, - ); - }); - } - - QueryBuilder musicsIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - 0, - true, - 0, - true, - ); - }); - } - - QueryBuilder musicsIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - 0, - false, - 999999, - true, - ); - }); - } - - QueryBuilder musicsLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - 0, - true, - length, - include, - ); - }); - } - - QueryBuilder musicsLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - length, - include, - 999999, - true, - ); - }); - } - - QueryBuilder musicsLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } -} - -extension PreviousPlaylistMusicsQueryObject on QueryBuilder< - PreviousPlaylistMusics, PreviousPlaylistMusics, QFilterCondition> {} - -extension PreviousPlaylistMusicsQueryLinks on QueryBuilder< - PreviousPlaylistMusics, PreviousPlaylistMusics, QFilterCondition> {} - -extension PreviousPlaylistMusicsQuerySortBy - on QueryBuilder {} - -extension PreviousPlaylistMusicsQuerySortThenBy on QueryBuilder< - PreviousPlaylistMusics, PreviousPlaylistMusics, QSortThenBy> { - QueryBuilder - thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } -} - -extension PreviousPlaylistMusicsQueryWhereDistinct - on QueryBuilder { - QueryBuilder - distinctByMusics() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'musics'); - }); - } -} - -extension PreviousPlaylistMusicsQueryProperty on QueryBuilder< - PreviousPlaylistMusics, PreviousPlaylistMusics, QQueryProperty> { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder?, QQueryOperations> - musicsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'musics'); - }); - } -} - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetPreviousPlaylistCurrentIndexCollection on Isar { - IsarCollection - get previousPlaylistCurrentIndexs => this.collection(); -} - -const PreviousPlaylistCurrentIndexSchema = CollectionSchema( - name: r'PreviousPlaylistCurrentIndex', - id: -9036091697598573141, - properties: { - r'currentIndex': PropertySchema( - id: 0, - name: r'currentIndex', - type: IsarType.long, - ), - r'mediaId': PropertySchema( - id: 1, - name: r'mediaId', - type: IsarType.long, - ) - }, - estimateSize: _previousPlaylistCurrentIndexEstimateSize, - serialize: _previousPlaylistCurrentIndexSerialize, - deserialize: _previousPlaylistCurrentIndexDeserialize, - deserializeProp: _previousPlaylistCurrentIndexDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - getId: _previousPlaylistCurrentIndexGetId, - getLinks: _previousPlaylistCurrentIndexGetLinks, - attach: _previousPlaylistCurrentIndexAttach, - version: '3.1.0', -); - -int _previousPlaylistCurrentIndexEstimateSize( - PreviousPlaylistCurrentIndex object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - return bytesCount; -} - -void _previousPlaylistCurrentIndexSerialize( - PreviousPlaylistCurrentIndex object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeLong(offsets[0], object.currentIndex); - writer.writeLong(offsets[1], object.mediaId); -} - -PreviousPlaylistCurrentIndex _previousPlaylistCurrentIndexDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = PreviousPlaylistCurrentIndex( - currentIndex: reader.readLongOrNull(offsets[0]), - id: id, - mediaId: reader.readLong(offsets[1]), - ); - return object; -} - -P _previousPlaylistCurrentIndexDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readLongOrNull(offset)) as P; - case 1: - return (reader.readLong(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _previousPlaylistCurrentIndexGetId(PreviousPlaylistCurrentIndex object) { - return object.id; -} - -List> _previousPlaylistCurrentIndexGetLinks( - PreviousPlaylistCurrentIndex object) { - return []; -} - -void _previousPlaylistCurrentIndexAttach( - IsarCollection col, Id id, PreviousPlaylistCurrentIndex object) { - object.id = id; -} - -extension PreviousPlaylistCurrentIndexQueryWhereSort on QueryBuilder< - PreviousPlaylistCurrentIndex, PreviousPlaylistCurrentIndex, QWhere> { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension PreviousPlaylistCurrentIndexQueryWhere on QueryBuilder< - PreviousPlaylistCurrentIndex, PreviousPlaylistCurrentIndex, QWhereClause> { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); - }); - } -} - -extension PreviousPlaylistCurrentIndexQueryFilter on QueryBuilder< - PreviousPlaylistCurrentIndex, - PreviousPlaylistCurrentIndex, - QFilterCondition> { - QueryBuilder currentIndexIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'currentIndex', - )); - }); - } - - QueryBuilder currentIndexIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'currentIndex', - )); - }); - } - - QueryBuilder currentIndexEqualTo(int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'currentIndex', - value: value, - )); - }); - } - - QueryBuilder currentIndexGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'currentIndex', - value: value, - )); - }); - } - - QueryBuilder currentIndexLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'currentIndex', - value: value, - )); - }); - } - - QueryBuilder currentIndexBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'currentIndex', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder mediaIdEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'mediaId', - value: value, - )); - }); - } - - QueryBuilder mediaIdGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'mediaId', - value: value, - )); - }); - } - - QueryBuilder mediaIdLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'mediaId', - value: value, - )); - }); - } - - QueryBuilder mediaIdBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'mediaId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } -} - -extension PreviousPlaylistCurrentIndexQueryObject on QueryBuilder< - PreviousPlaylistCurrentIndex, - PreviousPlaylistCurrentIndex, - QFilterCondition> {} - -extension PreviousPlaylistCurrentIndexQueryLinks on QueryBuilder< - PreviousPlaylistCurrentIndex, - PreviousPlaylistCurrentIndex, - QFilterCondition> {} - -extension PreviousPlaylistCurrentIndexQuerySortBy on QueryBuilder< - PreviousPlaylistCurrentIndex, PreviousPlaylistCurrentIndex, QSortBy> { - QueryBuilder sortByCurrentIndex() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'currentIndex', Sort.asc); - }); - } - - QueryBuilder sortByCurrentIndexDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'currentIndex', Sort.desc); - }); - } - - QueryBuilder sortByMediaId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mediaId', Sort.asc); - }); - } - - QueryBuilder sortByMediaIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mediaId', Sort.desc); - }); - } -} - -extension PreviousPlaylistCurrentIndexQuerySortThenBy on QueryBuilder< - PreviousPlaylistCurrentIndex, PreviousPlaylistCurrentIndex, QSortThenBy> { - QueryBuilder thenByCurrentIndex() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'currentIndex', Sort.asc); - }); - } - - QueryBuilder thenByCurrentIndexDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'currentIndex', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByMediaId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mediaId', Sort.asc); - }); - } - - QueryBuilder thenByMediaIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mediaId', Sort.desc); - }); - } -} - -extension PreviousPlaylistCurrentIndexQueryWhereDistinct on QueryBuilder< - PreviousPlaylistCurrentIndex, PreviousPlaylistCurrentIndex, QDistinct> { - QueryBuilder distinctByCurrentIndex() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'currentIndex'); - }); - } - - QueryBuilder distinctByMediaId() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'mediaId'); - }); - } -} - -extension PreviousPlaylistCurrentIndexQueryProperty on QueryBuilder< - PreviousPlaylistCurrentIndex, - PreviousPlaylistCurrentIndex, - QQueryProperty> { - QueryBuilder - idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder - currentIndexProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'currentIndex'); - }); - } - - QueryBuilder - mediaIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'mediaId'); - }); - } -} - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetPreviousPlaylistPositionCollection on Isar { - IsarCollection get previousPlaylistPositions => - this.collection(); -} - -const PreviousPlaylistPositionSchema = CollectionSchema( - name: r'PreviousPlaylistPosition', - id: 4468986416954213317, - properties: { - r'duration': PropertySchema( - id: 0, - name: r'duration', - type: IsarType.double, - ), - r'mediaId': PropertySchema( - id: 1, - name: r'mediaId', - type: IsarType.long, - ), - r'position': PropertySchema( - id: 2, - name: r'position', - type: IsarType.double, - ) - }, - estimateSize: _previousPlaylistPositionEstimateSize, - serialize: _previousPlaylistPositionSerialize, - deserialize: _previousPlaylistPositionDeserialize, - deserializeProp: _previousPlaylistPositionDeserializeProp, - idName: r'id', - indexes: {}, - links: {}, - embeddedSchemas: {}, - getId: _previousPlaylistPositionGetId, - getLinks: _previousPlaylistPositionGetLinks, - attach: _previousPlaylistPositionAttach, - version: '3.1.0', -); - -int _previousPlaylistPositionEstimateSize( - PreviousPlaylistPosition object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - return bytesCount; -} - -void _previousPlaylistPositionSerialize( - PreviousPlaylistPosition object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeDouble(offsets[0], object.duration); - writer.writeLong(offsets[1], object.mediaId); - writer.writeDouble(offsets[2], object.position); -} - -PreviousPlaylistPosition _previousPlaylistPositionDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = PreviousPlaylistPosition( - duration: reader.readDouble(offsets[0]), - id: id, - mediaId: reader.readLong(offsets[1]), - position: reader.readDouble(offsets[2]), - ); - return object; -} - -P _previousPlaylistPositionDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readDouble(offset)) as P; - case 1: - return (reader.readLong(offset)) as P; - case 2: - return (reader.readDouble(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -Id _previousPlaylistPositionGetId(PreviousPlaylistPosition object) { - return object.id; -} - -List> _previousPlaylistPositionGetLinks( - PreviousPlaylistPosition object) { - return []; -} - -void _previousPlaylistPositionAttach( - IsarCollection col, Id id, PreviousPlaylistPosition object) { - object.id = id; -} - -extension PreviousPlaylistPositionQueryWhereSort on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QWhere> { - QueryBuilder - anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } -} - -extension PreviousPlaylistPositionQueryWhere on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QWhereClause> { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan(Id id, {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); - }); - } -} - -extension PreviousPlaylistPositionQueryFilter on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QFilterCondition> { - QueryBuilder durationEqualTo( - double value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'duration', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder durationGreaterThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'duration', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder durationLessThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'duration', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder durationBetween( - double lower, - double upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'duration', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder idEqualTo(Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder mediaIdEqualTo(int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'mediaId', - value: value, - )); - }); - } - - QueryBuilder mediaIdGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'mediaId', - value: value, - )); - }); - } - - QueryBuilder mediaIdLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'mediaId', - value: value, - )); - }); - } - - QueryBuilder mediaIdBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'mediaId', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder positionEqualTo( - double value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'position', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder positionGreaterThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'position', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder positionLessThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'position', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder positionBetween( - double lower, - double upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'position', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } -} - -extension PreviousPlaylistPositionQueryObject on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QFilterCondition> {} - -extension PreviousPlaylistPositionQueryLinks on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QFilterCondition> {} - -extension PreviousPlaylistPositionQuerySortBy on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QSortBy> { - QueryBuilder - sortByDuration() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'duration', Sort.asc); - }); - } - - QueryBuilder - sortByDurationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'duration', Sort.desc); - }); - } - - QueryBuilder - sortByMediaId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mediaId', Sort.asc); - }); - } - - QueryBuilder - sortByMediaIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mediaId', Sort.desc); - }); - } - - QueryBuilder - sortByPosition() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'position', Sort.asc); - }); - } - - QueryBuilder - sortByPositionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'position', Sort.desc); - }); - } -} - -extension PreviousPlaylistPositionQuerySortThenBy on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QSortThenBy> { - QueryBuilder - thenByDuration() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'duration', Sort.asc); - }); - } - - QueryBuilder - thenByDurationDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'duration', Sort.desc); - }); - } - - QueryBuilder - thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder - thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder - thenByMediaId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mediaId', Sort.asc); - }); - } - - QueryBuilder - thenByMediaIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'mediaId', Sort.desc); - }); - } - - QueryBuilder - thenByPosition() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'position', Sort.asc); - }); - } - - QueryBuilder - thenByPositionDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'position', Sort.desc); - }); - } -} - -extension PreviousPlaylistPositionQueryWhereDistinct on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QDistinct> { - QueryBuilder - distinctByDuration() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'duration'); - }); - } - - QueryBuilder - distinctByMediaId() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'mediaId'); - }); - } - - QueryBuilder - distinctByPosition() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'position'); - }); - } -} - -extension PreviousPlaylistPositionQueryProperty on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QQueryProperty> { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder - durationProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'duration'); - }); - } - - QueryBuilder - mediaIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'mediaId'); - }); - } - - QueryBuilder - positionProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'position'); - }); - } -} diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart deleted file mode 100644 index 0e4adb61..00000000 --- a/packages/player/lib/src/queue.dart +++ /dev/null @@ -1,395 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -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, - this.onInitialize, - this.beforeInitialize, - }) : _shuffler = shuffler ?? SimpleShuffler() { - IsarService.instance.isarEnabled = initializeIsar; - itemsReady = !initializeIsar; - _initialize(); - } - Future _initialize() async { - 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; - } - } - }, - ); - } - - 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; - final Future Function()? onInitialize, beforeInitialize; - 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 - ? List.unmodifiable((storage.map((i) => i.item).toList())) - : []; - } - - Future> get previousItems async { - previousPlaylistMusics = - await IsarService.instance.getPreviousPlaylistMusics(); - return previousPlaylistMusics?.musics?.toListMedia ?? []; - } - - Future get _previousPlaylistPosition async { - final previousPlaylistPosition = - await IsarService.instance.getPreviousPlaylistPosition(); - return previousPlaylistPosition?.position != null - ? previousPlaylistPosition - : null; - } - - Future get previousPlaylistIndex async { - final previousPlaylistCurrentIndex = - await IsarService.instance.getPreviousPlaylistCurrentIndex(); - 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 saveOnTop = false, - }) async { - int i = storage.length == 1 ? 0 : storage.length - 1; - if (saveOnTop) { - storage.insertAll(0, _toQueueItems(items, i)); - } else { - storage.addAll(_toQueueItems(items, i)); - } - - await _save(medias: items, saveOnTop: saveOnTop); - } - - Future _save( - {required List medias, bool saveOnTop = false}) async { - final items = await previousItems; - debugPrint( - '[TESTE] itemsFromStorage: ${items.length} - mediasToSave: ${medias.length}', - ); - - await IsarService.instance.addPreviousPlaylistMusics( - PreviousPlaylistMusics(musics: organizeLists(saveOnTop, items, medias)), - ); - } - - 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}) { - 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; - } - - 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 diff > 3000; - } - } - return false; - } - - Media? possiblePrevious() { - if (index >= 0) { - var workIndex = index; - if (index > 0) { - --workIndex; - } - return storage[workIndex].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; - // updateIsarIndex(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 updateIsarIndex(int id, int newIndex) async { - IsarService.instance.addPreviousPlaylistCurrentIndex( - PreviousPlaylistCurrentIndex(mediaId: id, currentIndex: newIndex), - ); - } - - // 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; - 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(); - } -} diff --git a/packages/player/lib/src/queue/queue.dart b/packages/player/lib/src/queue/queue.dart new file mode 100644 index 00000000..32a0ea3b --- /dev/null +++ b/packages/player/lib/src/queue/queue.dart @@ -0,0 +1,393 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:smplayer/src/models/media.dart'; +import 'package:smplayer/src/models/previous_playlist_model.dart'; +import 'package:smplayer/src/models/queue_item.dart'; +import 'package:smplayer/src/enums/repeat_mode.dart'; +import 'package:smplayer/src/queue/shuffler.dart'; +import 'package:smplayer/src/queue/simple_shuffle.dart'; +import 'package:smplayer/src/services/isar_service.dart'; + +class Queue { + // ================ Constructor Parameters ================ + final Shuffler _shuffler; + final bool initializeIsar; + final Future Function(List)? onInitialize; + final Future Function()? beforeInitialize; + + // ================ Queue State ================ + var _index = 0; + Media? _current; + bool itemsReady = false; + List> playerQueue = >[]; + + // ================ Previous State ================ + int previousIndex = 0; + PreviousPlaylistPosition? previousPosition; + PreviousPlaylistMusics? previousPlaylistMusics; + DateTime? _lastPrevious; + + // ================ Getters and Setters ================ + int get index => _index; + + Media? get current => + _current ?? + (index >= 0 && index < playerQueue.length + ? playerQueue[index].item + : null); + + List get items => playerQueue.length > 0 + ? List.unmodifiable((playerQueue.map((i) => i.item).toList())) + : []; + + int get size => playerQueue.length; + + Media? get top => this.size > 0 ? playerQueue[0].item : null; + + set setIndex(int index) { + if (playerQueue.isEmpty || index < 0 || index >= playerQueue.length) { + _index = -1; + _current = null; + return; + } + + _index = index; + _current = playerQueue[index].item; + } + + // ================ ISAR ================ + + Future> get previousItems async { + previousPlaylistMusics = await IsarService.instance + .getPreviousPlaylistMusics(); + return previousPlaylistMusics?.musics?.toListMedia ?? []; + } + + Future get _previousPlaylistPosition async { + final previousPlaylistPosition = await IsarService.instance + .getPreviousPlaylistPosition(); + return previousPlaylistPosition?.position != null + ? previousPlaylistPosition + : null; + } + + Future get previousPlaylistIndex async { + final previousPlaylistCurrentIndex = await IsarService.instance + .getPreviousPlaylistCurrentIndex(); + return previousPlaylistCurrentIndex?.currentIndex ?? 0; + } + + Queue({ + Shuffler? shuffler, + this.initializeIsar = false, + this.onInitialize, + this.beforeInitialize, + }) : _shuffler = shuffler ?? SimpleShuffler() { + IsarService.instance.isarEnabled = initializeIsar; + itemsReady = !initializeIsar; + _initialize(); + } + + Future _initialize() async { + if (!itemsReady) { + try { + await beforeInitialize?.call(); + + final results = await Future.wait([ + previousItems, + previousPlaylistIndex, + _previousPlaylistPosition, + ]); + + final items = results[0] as List; + previousIndex = results[1] as int; + previousPosition = results[2] as PreviousPlaylistPosition?; + + if (items.isNotEmpty) { + playerQueue.addAll( + items.asMap().entries.map( + (entry) => QueueItem(entry.key, entry.key, entry.value), + ), + ); + await onInitialize?.call(items); + } + itemsReady = true; + } catch (e) { + debugPrint('#APP LOGS ==> Error initializing Queue: $e'); + itemsReady = true; + } + } + } + + List> _toQueueItems(List items, int i) { + return items.map((e) { + i++; + return QueueItem(i, i, e); + }).toList(); + } + + addAll(List items, {bool saveOnTop = false}) async { + int i = playerQueue.length == 1 ? 0 : playerQueue.length - 1; + if (saveOnTop) { + playerQueue.insertAll(0, _toQueueItems(items, i)); + } else { + playerQueue.addAll(_toQueueItems(items, i)); + } + + await _save(medias: items, saveOnTop: saveOnTop); + } + + Future _save({ + required List medias, + bool saveOnTop = false, + }) async { + final items = await previousItems; + debugPrint( + '[TESTE] itemsFromStorage: ${items.length} - mediasToSave: ${medias.length}', + ); + + await IsarService.instance.addPreviousPlaylistMusics( + PreviousPlaylistMusics(musics: organizeLists(saveOnTop, items, medias)), + ); + } + + /// Organizes two media lists into a single list of compressed strings. + /// + /// This function is responsible for combining two media lists (`items` and `medias`) into a single + /// list of strings, determining the order based on the `saveOnTop` parameter. + /// + /// Parameters: + /// * [saveOnTop] - A boolean that determines the order of list combination: + /// - If `true`: the `medias` list will be placed on top + /// - If `false`: the `items` list will be placed on top + /// * [items] - First media list to be combined + /// * [medias] - Second media list to be combined + /// + /// Returns: + /// * A list of strings containing the compressed representations of the media in the determined order + /// + /// Usage example: + /// ```dart + /// final result = organizeLists( + /// saveOnTop: true, + /// items: [media1, media2], + /// medias: [media3, media4] + /// ); + /// // If saveOnTop is true, the result will be [media3, media4, media1, media2] + /// // If saveOnTop is false, the result will be [media1, media2, media3, media4] + /// ``` + /// + /// Notes: + /// * The function uses the `toListStringCompressed` method of the media lists to convert + /// Media objects into compressed strings + /// * The order of the lists is determined by the `saveOnTop` parameter, which controls which list + /// will be placed at the beginning of the resulting list + /// * This function is commonly used to organize playlists and manage the playback order + /// of media in the player + 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, + }) { + try { + int lastLength = playerQueue.length; + for (var i = 0; i < positionsToDelete.length; ++i) { + final pos = positionsToDelete[i] - i; + if (pos < index) { + setIndex = index - 1; + } + playerQueue.removeAt(pos); + } + + for (var j = 0; j < playerQueue.length; ++j) { + playerQueue[j].position = j; + } + + if (kDebugMode) { + for (var e in playerQueue) { + debugPrint( + '=====> storage remove: ${e.item.name} - ${e.position} | ${e.originalPosition}', + ); + } + } + return lastLength - playerQueue.length; + } catch (e) { + return 0; + } + } + + clear() => removeAll(); + + removeAll() { + playerQueue.clear(); + setIndex = 0; + } + + shuffle() { + if (playerQueue.length > 1) { + var current = playerQueue[index]; + _shuffler.shuffle(playerQueue); + for (var i = 0; i < playerQueue.length; ++i) { + playerQueue[i].position = i; + } + var currentIndex = playerQueue.indexOf(current); + reorder(currentIndex, 0, true); + setIndex = 0; + } + } + + unshuffle() { + if (playerQueue.length > 1) { + var current = playerQueue[index]; + _shuffler.unshuffle(playerQueue); + for (var i = 0; i < playerQueue.length; ++i) { + final item = playerQueue[i]; + item.position = i; + } + if (kDebugMode) { + for (var e in playerQueue) { + debugPrint( + '=====> storage unshuffle: ${e.item.name} - ${e.position} | ${e.originalPosition}', + ); + } + } + setIndex = current.position; + } + } + + 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 diff > 3000; + } + } + return false; + } + + Media? possiblePrevious() { + if (index >= 0) { + var workIndex = index; + if (index > 0) { + --workIndex; + } + return playerQueue[workIndex].item; + } + return playerQueue.isNotEmpty && index >= 0 + ? playerQueue[index].item + : 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 (playerQueue.length - 1 == index) { + return playerQueue[0].item; + } else { + return _next(); + } + } else { + return null; + } + } + + Media? _next() { + if (playerQueue.length == 0) { + return null; + } else if (playerQueue.length > 0 && index < playerQueue.length - 1) { + var media = playerQueue[index + 1].item; + return media; + } else { + return null; + } + } + + Media? move(int pos) { + if (playerQueue.length == 0) { + throw AssertionError("Queue is empty"); + } else if (playerQueue.length > 0 && pos <= playerQueue.length - 1) { + var media = playerQueue[pos].item; + setIndex = pos; + return media; + } else { + return null; + } + } + + void updateIsarIndex(int id, int newIndex) async { + IsarService.instance.addPreviousPlaylistCurrentIndex( + PreviousPlaylistCurrentIndex(mediaId: id, currentIndex: newIndex), + ); + } + + Media restart() { + setIndex = 0; + return playerQueue.first.item; + } + + reorder(int oldIndex, int newIndex, [bool isShuffle = false]) { + final playingItem = playerQueue.elementAt(index); + if (newIndex > oldIndex) { + for (int i = oldIndex + 1; i <= newIndex; i++) { + if (!isShuffle) { + playerQueue[i].originalPosition--; + } + playerQueue[i].position--; + } + } else { + for (int i = newIndex; i < oldIndex; i++) { + if (!isShuffle) { + playerQueue[i].originalPosition++; + } + playerQueue[i].position++; + } + } + + playerQueue[oldIndex].position = newIndex; + if (!isShuffle) { + playerQueue[oldIndex].originalPosition = newIndex; + } + playerQueue.sort((a, b) => a.position.compareTo(b.position)); + final playingIndex = playerQueue.indexOf(playingItem); + + if (kDebugMode) { + debugPrint( + '=====> ${playerQueue[oldIndex].item.name} - playerQueue[oldIndex]: ${playerQueue[oldIndex].originalPosition}', + ); + debugPrint( + '=====> ${playerQueue[newIndex].item.name} - playerQueue[newIndex]: ${playerQueue[newIndex].originalPosition}', + ); + for (var e in playerQueue) { + debugPrint( + '=====> storage Reorder: ${e.item.name} - ${e.position} - ${e.originalPosition}', + ); + } + } + setIndex = playingIndex; + } + + void dispose() { + IsarService.instance.dispose(); + } +} diff --git a/packages/player/lib/src/shuffler.dart b/packages/player/lib/src/queue/shuffler.dart similarity index 60% rename from packages/player/lib/src/shuffler.dart rename to packages/player/lib/src/queue/shuffler.dart index d8899ae2..7ef532ca 100644 --- a/packages/player/lib/src/shuffler.dart +++ b/packages/player/lib/src/queue/shuffler.dart @@ -1,6 +1,5 @@ -import 'package:smplayer/src/queue_item.dart'; - -import '../player.dart'; +import 'package:smplayer/src/models/media.dart'; +import 'package:smplayer/src/models/queue_item.dart'; abstract class Shuffler { List> shuffle(List> list); diff --git a/packages/player/lib/src/simple_shuffle.dart b/packages/player/lib/src/queue/simple_shuffle.dart similarity index 71% rename from packages/player/lib/src/simple_shuffle.dart rename to packages/player/lib/src/queue/simple_shuffle.dart index 8770de9a..a9ca3402 100644 --- a/packages/player/lib/src/simple_shuffle.dart +++ b/packages/player/lib/src/queue/simple_shuffle.dart @@ -1,6 +1,6 @@ -import 'package:smplayer/src/queue_item.dart'; -import 'package:smplayer/src/shuffler.dart'; -import 'package:smplayer/src/media.dart'; +import 'package:smplayer/src/models/media.dart'; +import 'package:smplayer/src/models/queue_item.dart'; +import 'package:smplayer/src/queue/shuffler.dart'; class SimpleShuffler extends Shuffler { List> shuffle(List> list) { diff --git a/packages/player/lib/src/release_mode.dart b/packages/player/lib/src/release_mode.dart deleted file mode 100644 index 4a0bed20..00000000 --- a/packages/player/lib/src/release_mode.dart +++ /dev/null @@ -1,5 +0,0 @@ -enum ReleaseMode { - RELEASE, - LOOP, - STOP -} \ No newline at end of file diff --git a/packages/player/lib/src/isar_service.dart b/packages/player/lib/src/services/isar_service.dart similarity index 67% rename from packages/player/lib/src/isar_service.dart rename to packages/player/lib/src/services/isar_service.dart index 804e1aa4..07a3b73c 100644 --- a/packages/player/lib/src/isar_service.dart +++ b/packages/player/lib/src/services/isar_service.dart @@ -5,7 +5,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:smplayer/src/previous_playlist_model.dart'; +import 'package:smplayer/src/models/previous_playlist_model.dart'; class IsarService { IsarService._(); @@ -23,11 +23,13 @@ class IsarService { if (_isarStorage == null && _isIsarEnabled) { debugPrint('Initializing IsarStorage'); if (Platform.isMacOS || Platform.isLinux) { - Isar.initializeIsarCore(libraries: { - Abi.macosArm64: 'libisar_macos.dylib', - Abi.macosX64: 'libisar_macos.dylib', - Abi.linuxX64: 'libisar_linux_x64.so', - }); + Isar.initializeIsarCore( + libraries: { + Abi.macosArm64: 'libisar_macos.dylib', + Abi.macosX64: 'libisar_macos.dylib', + Abi.linuxX64: 'libisar_linux_x64.so', + }, + ); } if (!(_isarStorage?.isOpen ?? false)) { @@ -58,13 +60,9 @@ class IsarService { ) async { await initializeIfNeeded(); try { - await _isarStorage?.writeTxn( - () async { - await _isarStorage?.previousPlaylistMusics - .put(previousPlaylistMusics); - }, - silent: kDebugMode, - ); + await _isarStorage?.writeTxn(() async { + await _isarStorage?.previousPlaylistMusics.put(previousPlaylistMusics); + }, silent: kDebugMode); } catch (_) {} } @@ -78,18 +76,16 @@ class IsarService { ) async { await initializeIfNeeded(); try { - await _isarStorage?.writeTxn( - () async { - await _isarStorage?.previousPlaylistCurrentIndexs - .put(previousPlaylistCurrentIndex); - }, - silent: kDebugMode, - ); + await _isarStorage?.writeTxn(() async { + await _isarStorage?.previousPlaylistCurrentIndexs.put( + previousPlaylistCurrentIndex, + ); + }, silent: kDebugMode); } catch (_) {} } Future - getPreviousPlaylistCurrentIndex() async { + getPreviousPlaylistCurrentIndex() async { await initializeIfNeeded(); return _isarStorage?.previousPlaylistCurrentIndexs.getSync(1); } @@ -99,13 +95,11 @@ class IsarService { ) async { await initializeIfNeeded(); try { - await _isarStorage?.writeTxn( - () async { - await _isarStorage?.previousPlaylistPositions - .put(previousPlaylistPosition); - }, - silent: kDebugMode, - ); + await _isarStorage?.writeTxn(() async { + await _isarStorage?.previousPlaylistPositions.put( + previousPlaylistPosition, + ); + }, silent: kDebugMode); } catch (_) {} } @@ -114,10 +108,8 @@ class IsarService { return _isarStorage?.previousPlaylistPositions.getSync(1); } - Future removeAllMusics() async => await _isarStorage?.writeTxn( - () async { - await _isarStorage?.clear(); - }, - silent: kDebugMode, - ); + Future removeAllMusics() async => + await _isarStorage?.writeTxn(() async { + await _isarStorage?.clear(); + }, silent: kDebugMode); } diff --git a/packages/player/pubspec.yaml b/packages/player/pubspec.yaml index adf86269..7df3d143 100644 --- a/packages/player/pubspec.yaml +++ b/packages/player/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/suamusica publish_to: none environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.8.0 <4.0.0" isar_version: &isar_version 3.1.0 @@ -19,11 +19,15 @@ 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 path: packages/aws/ + mdns_plugin: + git: + url: https://github.com/SuaMusica/flutter + path: mdns_plugin + ref: develop dev_dependencies: build_runner: ^2.2.1 diff --git a/packages/player/test/player_test.dart b/packages/player/test/player_test.dart index 3ab769a1..8f94bfa1 100644 --- a/packages/player/test/player_test.dart +++ b/packages/player/test/player_test.dart @@ -11,70 +11,72 @@ void main() { const MethodChannel channel = MethodChannel('suamusica.com.br/player'); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - channel, - (MethodCall methodCall) async { - return Player.Ok; - }, - ); + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return Player.Ok; + }); final media1 = Media( - id: 1, - albumTitle: "Album", - albumId: 2, - name: "O Bebe", - 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, - ownerId: 2, - shareUrl: ""); + id: 1, + albumTitle: "Album", + albumId: 2, + name: "O Bebe", + 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, + ownerId: 2, + shareUrl: "", + ); final media2 = Media( - id: 2, - albumTitle: "Album", - albumId: 2, - ownerId: 2, - name: "Solteiro Largado", - 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: ""); + id: 2, + albumTitle: "Album", + albumId: 2, + ownerId: 2, + name: "Solteiro Largado", + 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: "", + ); final media3 = Media( - id: 3, - albumTitle: "Album", - ownerId: 2, - albumId: 2, - name: "Borrachinha", - url: "https://android.suamusica.com.br/373377/2238511/05+Borrachinha.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: false, - shareUrl: ""); + id: 3, + albumTitle: "Album", + ownerId: 2, + albumId: 2, + name: "Borrachinha", + url: "https://android.suamusica.com.br/373377/2238511/05+Borrachinha.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: false, + shareUrl: "", + ); group('Player operations', () { - test('Adding media to an empty queue shall make it the queue top', - () async { - final subject = createPlayer(); - subject.enqueue(media: media1); - expect(subject.size, 1); - expect(subject.top, media1); - }); + test( + 'Adding media to an empty queue shall make it the queue top', + () async { + final subject = createPlayer(); + 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(media: media1); @@ -156,46 +158,44 @@ void main() { final subject = createPlayer(); expect(subject.rewind(), throwsAssertionError); }); - test('Rewind on a queue that was not played shall raise an error', - () async { - final subject = createPlayer(); - subject.enqueue(media: media1); - subject.enqueue(media: media2); - subject.enqueue(media: media3); - expect(await subject.rewind(), 1); - }); test( - 'Rewind shall be supported', + 'Rewind on a queue that was not played shall raise an error', () async { final subject = createPlayer(); subject.enqueue(media: media1); subject.enqueue(media: media2); subject.enqueue(media: media3); - subject.play(media1); - - subject.rewind(); - - expect(subject.size, 3); - expect(subject.top, media1); - expect(subject.items, [media1, media2, media3]); - }, - ); - test( - 'Previous on empty queue shall raise an error', - () async { - final subject = createPlayer(); - expect(() => subject.previous(), throwsRangeError); + expect(await subject.rewind(), 1); }, ); - test('Previous on a queue that was not played shall raise an error', - () async { + test('Rewind shall be supported', () async { final subject = createPlayer(); subject.enqueue(media: media1); subject.enqueue(media: media2); subject.enqueue(media: media3); - final a = await subject.previous(); - expect(a, 1); + subject.play(media1); + + subject.rewind(); + + expect(subject.size, 3); + expect(subject.top, media1); + expect(subject.items, [media1, media2, media3]); }); + test('Previous on empty queue shall raise an error', () async { + final subject = createPlayer(); + expect(() => subject.previous(), throwsRangeError); + }); + test( + 'Previous on a queue that was not played shall raise an error', + () async { + final subject = createPlayer(); + 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(media: media1); @@ -210,71 +210,75 @@ void main() { expect(subject.items, [media1, media2, media3]); }); test( - 'Two consecutive previous invocation shall really go the previous track', - () async { - final subject = createPlayer(); - subject.enqueue(media: media1); - subject.enqueue(media: media2); - subject.enqueue(media: media3); + 'Two consecutive previous invocation shall really go the previous track', + () async { + final subject = createPlayer(); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); - expect(await subject.next(), Player.Ok); - expect(subject.size, 3); - expect(subject.current, media2); - expect(subject.items, [media1, media2, media3]); + expect(await subject.next(), Player.Ok); + expect(subject.size, 3); + expect(subject.current, media2); + expect(subject.items, [media1, media2, media3]); - expect(await subject.next(), Player.Ok); - expect(subject.size, 3); - expect(subject.current, media3); - expect(subject.items, [media1, media2, media3]); + expect(await subject.next(), Player.Ok); + expect(subject.size, 3); + expect(subject.current, media3); + expect(subject.items, [media1, media2, media3]); - expect(await subject.previous(), Player.Ok); - expect(await subject.previous(), Player.Ok); - expect(subject.size, 3); - expect(subject.current, media2); - expect(subject.items, [media1, media2, media3]); - }); + expect(await subject.previous(), Player.Ok); + expect(await subject.previous(), Player.Ok); + expect(subject.size, 3); + expect(subject.current, media2); + expect(subject.items, [media1, media2, media3]); + }, + ); test( - 'Two consecutive previous invocation with interval greater than 1 sec shall solely rewind', - () async { - final subject = createPlayer(); + 'Two consecutive previous invocation with interval greater than 1 sec shall solely rewind', + () async { + final subject = createPlayer(); - subject.enqueue(media: media1); - subject.enqueue(media: media2); - subject.enqueue(media: media3); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); - expect(await subject.next(), Player.Ok); - expect(subject.size, 3); - expect(subject.current, media2); - expect(subject.items, [media1, media2, media3]); + expect(await subject.next(), Player.Ok); + expect(subject.size, 3); + expect(subject.current, media2); + expect(subject.items, [media1, media2, media3]); - expect(await subject.next(), Player.Ok); - expect(subject.size, 3); - expect(subject.current, media3); - expect(subject.items, [media1, media2, media3]); + expect(await subject.next(), Player.Ok); + expect(subject.size, 3); + expect(subject.current, media3); + expect(subject.items, [media1, media2, media3]); - expect(await subject.previous(), Player.Ok); - sleep(Duration(seconds: 3)); - expect(await subject.previous(), Player.Ok); - expect(subject.size, 3); - expect(subject.current, media3); - expect(subject.items, [media1, media2, media3]); - }); + expect(await subject.previous(), Player.Ok); + sleep(Duration(seconds: 3)); + expect(await subject.previous(), Player.Ok); + expect(subject.size, 3); + expect(subject.current, media3); + expect(subject.items, [media1, media2, media3]); + }, + ); test('Next on empty queue shall raise an error', () async { final subject = createPlayer(); expect(await subject.next(), null); }); - test('Next on a queue that was not played shall start playing it', - () async { - final subject = createPlayer(); - subject.enqueue(media: media1); - subject.enqueue(media: media2); - subject.enqueue(media: media3); + test( + 'Next on a queue that was not played shall start playing it', + () async { + final subject = createPlayer(); + subject.enqueue(media: media1); + subject.enqueue(media: media2); + subject.enqueue(media: media3); - expect(await subject.next(), Player.Ok); - expect(subject.size, 3); - expect(subject.top, media1); - expect(subject.items, [media1, media2, media3]); - }); + expect(await subject.next(), Player.Ok); + expect(subject.size, 3); + expect(subject.top, media1); + expect(subject.items, [media1, media2, media3]); + }, + ); test('Next on a queue that is playing shall move to the next', () async { final subject = createPlayer(); @@ -339,15 +343,17 @@ void main() { expect(subject.top, null); expect(subject.items, []); }); - test('Top on an unplayed queue shall return the top of the queue', - () async { - final subject = createPlayer(); - subject.enqueue(media: media1); - subject.enqueue(media: media2); - subject.enqueue(media: media3); - expect(subject.size, 3); - expect(subject.top, media1); - }); + test( + 'Top on an unplayed queue shall return the top of the queue', + () async { + final subject = createPlayer(); + 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(media: media1); @@ -374,12 +380,12 @@ void main() { } Player createPlayer() => Player( - cookieSigner: cookieSigner, - autoPlay: false, - playerId: "smplayer", - localMediaValidator: null, - initializeIsar: false, - ); + cookieSigner: cookieSigner, + autoPlay: false, + playerId: "smplayer", + localMediaValidator: null, + initializeIsar: false, +); Future cookieSigner() async { DateTime expiresOn = DateTime.now().add(Duration(hours: 12)); @@ -394,10 +400,7 @@ Future cookieSigner() async { class SMPlayer extends StatefulWidget { final player; - SMPlayer({ - Key? key, - this.player, - }) : super(key: key); + SMPlayer({Key? key, this.player}) : super(key: key); @override State createState() => _SMPlayerState(player); diff --git a/packages/player/test/queue_test.dart b/packages/player/test/queue_test.dart index 88596d80..89d08756 100644 --- a/packages/player/test/queue_test.dart +++ b/packages/player/test/queue_test.dart @@ -1,72 +1,74 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; +import 'package:smplayer/player.dart'; import 'package:smplayer/src/media.dart'; import 'package:smplayer/src/queue.dart'; void main() { final media1 = Media( - id: 1, - albumTitle: "Album", - albumId: 2, - ownerId: 2, - name: "O Bebe", - 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: ""); + id: 1, + albumTitle: "Album", + albumId: 2, + ownerId: 2, + name: "O Bebe", + 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: "", + ); final media2 = Media( - id: 2, - albumTitle: "Album", - albumId: 2, - ownerId: 2, - name: "Solteiro Largado", - 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: ""); + id: 2, + albumTitle: "Album", + albumId: 2, + ownerId: 2, + name: "Solteiro Largado", + 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: "", + ); final media3 = Media( - id: 3, - albumTitle: "Album", - albumId: 2, - ownerId: 2, - name: "Borrachinha", - url: "https://android.suamusica.com.br/373377/2238511/05+Borrachinha.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: false, - shareUrl: ""); + id: 3, + albumTitle: "Album", + albumId: 2, + ownerId: 2, + name: "Borrachinha", + url: "https://android.suamusica.com.br/373377/2238511/05+Borrachinha.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: false, + shareUrl: "", + ); group('Queue operations', () { test('Adding media to an empty queue shall make it the queue top', () { final subject = Queue(); - subject.add(media1); + subject.addAll([media1]); expect(subject.size, 1); expect(subject.top, media1); }); test('The queue shall support multiple items', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); + subject.addAll([media1, media2, media3]); expect(subject.size, 3); expect(subject.top, media1); expect(subject.items, [media1, media2, media3]); @@ -74,23 +76,21 @@ void main() { test('Playing a media shall replace the queue top', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.play(media3); + subject.addAll([media1, media2, media3]); - expect(subject.size, 2); - expect(subject.top, media3); - expect(subject.items, [media3, media2]); + expect(subject.size, 3); + expect(subject.top, media1); + expect(subject.items, [media1, media2, media3]); }); test('Removing a media shall be supported', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); + subject.addAll([media1, media2, media3]); subject.removeByPosition( - positionsToDelete: [subject.storage[1].position], isShuffle: false); + positionsToDelete: [subject.playerQueue[1].position], + isShuffle: false, + ); expect(subject.size, 2); expect(subject.top, media1); @@ -145,34 +145,22 @@ void main() { expect(subject.items, items); }); - test( - 'Rewind on empty queue shall raise an error', - () { - final subject = Queue(); - expect( - () => subject.rewind(), - throwsAssertionError, - ); - }, - ); + test('Rewind on empty queue shall raise an error', () { + final subject = Queue(); + expect(subject.shouldRewind(), false); + }); test('Rewind on a queue that was not played shall raise an error', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); - - expect(subject.rewind(), media1); + subject.addAll([media1, media2, media3]); + final shouldRewind = subject.shouldRewind(); + final rewind = shouldRewind ? subject.possiblePrevious() : null; + expect(rewind, shouldRewind ? null : media1); }); test('Rewind shall be supported', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); - subject.play(media1); - - subject.rewind(); + subject.addAll([media1, media2, media3]); expect(subject.size, 3); expect(subject.top, media1); @@ -181,149 +169,136 @@ void main() { test('Previous on empty queue shall raise an error', () { final subject = Queue(); - expect(() => subject.previous(), throwsAssertionError); + expect(() => subject.possiblePrevious(), throwsAssertionError); }); test('Previous on a queue', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); + subject.addAll([media1, media2, media3]); - expect(subject.previous(), media1); + expect(subject.possiblePrevious(), media1); }); test('Previous shall act as rewind', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); - subject.play(media1); + subject.addAll([media1, media2, media3]); - subject.previous(); + subject.possiblePrevious(); expect(subject.size, 3); expect(subject.top, media1); expect(subject.items, [media1, media2, media3]); }); test( - 'Two consecutive previous invocation shall really go the previous track', - () { - final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); + 'Two consecutive previous invocation shall really go the previous track', + () { + final subject = Queue(); + subject.addAll([media1, media2, media3]); - final next1 = subject.next(); - expect(subject.size, 3); - expect(subject.current, media2); - expect(next1, media2); - expect(subject.items, [media1, media2, media3]); + final next1 = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); + expect(subject.size, 3); + expect(subject.current, media2); + expect(next1, media2); + expect(subject.items, [media1, media2, media3]); - final next2 = subject.next(); - expect(subject.size, 3); - expect(subject.current, media3); - expect(next2, media3); - expect(subject.items, [media1, media2, media3]); + final next2 = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); + expect(subject.size, 3); + expect(subject.current, media3); + expect(next2, media3); + expect(subject.items, [media1, media2, media3]); - subject.previous(); - final previous = subject.previous(); - expect(subject.size, 3); - expect(subject.current, media2); - expect(previous, media2); - expect(subject.items, [media1, media2, media3]); - }); + subject.possiblePrevious(); + final previous = subject.possiblePrevious(); + expect(subject.size, 3); + expect(subject.current, media2); + expect(previous, media2); + expect(subject.items, [media1, media2, media3]); + }, + ); test( - 'Two consecutive previous invocation with interval greater than 1 sec shall solely rewind', - () { - final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); + 'Two consecutive previous invocation with interval greater than 1 sec shall solely rewind', + () { + final subject = Queue(); + subject.addAll([media1, media2, media3]); - final next1 = subject.next(); - expect(subject.size, 3); - expect(subject.current, media2); - expect(next1, media2); - expect(subject.items, [media1, media2, media3]); + final next1 = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); + expect(subject.size, 3); + expect(subject.current, media2); + expect(next1, media2); + expect(subject.items, [media1, media2, media3]); - final next2 = subject.next(); - expect(subject.size, 3); - expect(subject.current, media3); - expect(next2, media3); - expect(subject.items, [media1, media2, media3]); + final next2 = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); + expect(subject.size, 3); + expect(subject.current, media3); + expect(next2, media3); + expect(subject.items, [media1, media2, media3]); - subject.previous(); - sleep(Duration(seconds: 3)); - final previous = subject.previous(); - expect(subject.size, 3); - expect(subject.current, media3); - expect(previous, media3); - expect(subject.items, [media1, media2, media3]); - }); + subject.possiblePrevious(); + sleep(Duration(seconds: 3)); + final previous = subject.possiblePrevious(); + expect(subject.size, 3); + expect(subject.current, media3); + expect(previous, media3); + expect(subject.items, [media1, media2, media3]); + }, + ); test('Next on empty queue shall raise an error', () { final subject = Queue(); - expect(() => subject.next(), throwsAssertionError); + expect( + () => subject.possibleNext(RepeatMode.REPEAT_MODE_OFF), + throwsAssertionError, + ); }); test('Next on a queue that was not played shall start playing it', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); + subject.addAll([media1, media2, media3]); - final next = subject.next(); + final next = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); expect(subject.size, 3); expect(subject.top, media1); expect(next, media2); expect(subject.items, [media1, media2, media3]); }); - test( - 'Next on a queue that is playing shall move to the next', - () { - final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); + test('Next on a queue that is playing shall move to the next', () { + final subject = Queue(); + subject.addAll([media1, media2, media3]); - final next1 = subject.next(); - expect(subject.size, 3); - expect(subject.current, media2); - expect(next1, media2); - expect(subject.items, [media1, media2, media3]); + final next1 = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); + expect(subject.size, 3); + expect(subject.current, media2); + expect(next1, media2); + expect(subject.items, [media1, media2, media3]); - final next2 = subject.next(); - expect(subject.size, 3); - expect(subject.current, media3); - expect(next2, media3); - expect(subject.items, [media1, media2, media3]); - }, - ); + final next2 = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); + expect(subject.size, 3); + expect(subject.current, media3); + expect(next2, media3); + expect(subject.items, [media1, media2, media3]); + }); test('Next when reaching the end of the queue shall return null', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); + subject.addAll([media1, media2, media3]); - final next1 = subject.next(); + final next1 = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); expect(subject.size, 3); expect(subject.current, media2); expect(next1, media2); expect(subject.items, [media1, media2, media3]); - final next2 = subject.next(); + final next2 = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); expect(subject.size, 3); expect(subject.current, media3); expect(next2, media3); expect(subject.items, [media1, media2, media3]); - final next3 = subject.next(); + final next3 = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); expect(subject.size, 3); expect(subject.current, media3); expect(next3, null); expect(subject.items, [media1, media2, media3]); - final next4 = subject.next(); + final next4 = subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); expect(subject.size, 3); expect(subject.current, media3); expect(next4, null); @@ -337,7 +312,7 @@ void main() { items.addAll([media1, media2, media3]); } subject.addAll(items); - subject.next(); + subject.possibleNext(RepeatMode.REPEAT_MODE_OFF); expect(subject.size, 3 * interactions); expect(subject.top, media1); expect(subject.items, items); @@ -349,20 +324,91 @@ void main() { }); test('Top on an unplayed queue shall return the top of the queue', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); + subject.addAll([media1, media2, media3]); expect(subject.size, 3); expect(subject.top, media1); }); test('Current on an unplayed queue shall return null', () { final subject = Queue(); - subject.add(media1); - subject.add(media2); - subject.add(media3); + subject.addAll([media1, media2, media3]); expect(subject.size, 3); expect(subject.current, media1); }); }); + + group('Queue reorder operations', () { + test('Reorder shall move an item to a new position', () { + final subject = Queue(); + subject.addAll([media1, media2, media3]); + + subject.reorder(0, 2); + + expect(subject.size, 3); + expect(subject.items, [media2, media3, media1]); + }); + + test('Reorder shall maintain correct positions after moving an item', () { + final subject = Queue(); + subject.addAll([media1, media2, media3]); + + subject.reorder(0, 2); + + expect(subject.size, 3); + expect(subject.items, [media2, media3, media1]); + expect(subject.top, media2); + }); + + test('Reorder shall maintain current playing item position', () { + final subject = Queue(); + subject.addAll([media1, media2, media3]); + subject.reorder(0, 2); + expect(subject.size, 3); + expect(subject.items, [media2, media3, media1]); + expect(subject.current, media2); + }); + + test('Reorder shall handle moving an item to its current position', () { + final subject = Queue(); + subject.addAll([media1, media2, media3]); + + subject.reorder(1, 1); + + expect(subject.size, 3); + expect(subject.items, [media1, media2, media3]); + }); + + test('Reorder shall handle moving an item to the beginning', () { + final subject = Queue(); + subject.addAll([media1, media2, media3]); + + subject.reorder(2, 0); + + expect(subject.size, 3); + expect(subject.items, [media3, media1, media2]); + expect(subject.top, media3); + }); + + test('Reorder shall handle moving an item to the end', () { + final subject = Queue(); + subject.addAll([media1, media2, media3]); + + subject.reorder(0, 2); + + expect(subject.size, 3); + expect(subject.items, [media2, media3, media1]); + }); + + test('Reorder shall maintain correct positions in shuffled mode', () { + final subject = Queue(); + subject.addAll([media1, media2, media3]); + subject.shuffle(); + + subject.reorder(0, 2, true); + + expect(subject.size, 3); + expect(subject.items.length, 3); + expect(subject.top, isNotNull); + }); + }); }