From 1fa075c7a72d6827f3986ce02219b070f1f0a894 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 18 Dec 2024 16:15:09 -0300 Subject: [PATCH 01/34] added interruptionObserver --- .../player/ios/Classes/PlayerPlugin.swift | 1 + packages/player/ios/Classes/SMPlayer.swift | 28 +++++++++++++++++++ .../ios/Classes/SMPlayerListeners.swift | 4 +-- packages/player/lib/src/player.dart | 5 ++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index c9ea0c29..9bb1c75d 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -150,6 +150,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/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 7827fa55..00c62261 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -39,6 +39,13 @@ public class SMPlayer : NSObject { self.methodChannelManager = methodChannelManager listeners = SMPlayerListeners(smPlayer:smPlayer,methodChannelManager:methodChannelManager) listeners?.addPlayerObservers() + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleInterruption(_:)), + name: AVAudioSession.interruptionNotification, + object: nil + ) listeners?.onMediaChanged = { [self] in if(self.smPlayer.items().count > 0){ @@ -58,6 +65,27 @@ public class SMPlayer : NSObject { setupNowPlayingInfoCenter() _ = AudioSessionManager.activeSession() } + + @objc private func handleInterruption(_ notification: Notification) { + guard let userInfo = notification.userInfo, + let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { + return + } + switch type { + case .began: + pause() + case .ended: + if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { + let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) + if options.contains(.shouldResume) { + play() + } + } + @unknown default: + break + } + } func pause() { smPlayer.pause() diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index e399816b..3ce59162 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -28,7 +28,8 @@ public class SMPlayerListeners : NSObject { 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 { + if let error = playerItem.error { + print("#NATIVE LOGS ==> ERROR: \(String(describing: playerItem.error))") self.methodChannelManager?.notifyError(error: "UNKNOW ERROR") } } @@ -132,7 +133,6 @@ public class SMPlayerListeners : NSObject { notPlayingReason?.invalidate() playback?.invalidate() mediaChange?.invalidate() - mediaChange = nil notPlayingReason = nil playback = nil diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index e8738172..65b5bb14 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -383,6 +383,11 @@ class Player { required bool isFavorite, required int id, }) async { + // final index = _queue.items.indexWhere((item) => item.id == id); + // if (index != -1) { + // _queue.items[index] = + // _queue.items[index].copyWith(isFavorite: isFavorite); + // } return _channel.invokeMethod('update_notification', { 'isFavorite': isFavorite, 'idFavorite': id, From 37013f986a6e32306e9cca3e050f783f605fdd0e Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 20 Dec 2024 14:15:20 -0300 Subject: [PATCH 02/34] cast working v1 --- .../kotlin/br/com/suamusica/player/Cast.kt | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 packages/player/android/src/main/kotlin/br/com/suamusica/player/Cast.kt 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..3c9bafdf --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/Cast.kt @@ -0,0 +1,253 @@ +package br.com.suamusica.player + +import android.R +import android.content.Context +import android.content.Intent +import android.media.MediaRouter.RouteInfo.DEVICE_TYPE_TV +import android.net.Uri +import android.util.Log +import androidx.media3.cast.SessionAvailabilityListener +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +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 br.com.suamusica.player.PlayerPlugin.Companion.cookie +import com.google.android.gms.cast.* +import com.google.android.gms.cast.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 +import com.google.android.gms.common.images.WebImage +import java.util.WeakHashMap +import java.util.concurrent.Executors + + +@UnstableApi +class Cast( + private val context: Context, + private val player: ExoPlayer? = null, + private val cookie: String = "", + private val mediaItemMediaAssociations: WeakHashMap = WeakHashMap(), + + ) : + SessionAvailabilityListener, + CastStateListener, + Cast.Listener(), + SessionManagerListener, + PendingResult.StatusListener { + companion object { + const val TAG = "Chromecast" + + + } + + private var mediaRouter = MediaRouter.getInstance(context) + private var isConnected = false + private var castContext: CastContext? = null + private var sessionManager: SessionManager? = null + private var mediaRouterCallback: MediaRouter.Callback? = null + + init { + castContext = CastContext.getSharedInstance(context) + castContext?.addCastStateListener(this) + sessionManager = castContext?.sessionManager + + 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()) + player?.seekTo(3,0) + loadMedia(player?.currentMediaItem?.associatedMedia?.name!!, "Artist", Uri.parse(player.currentMediaItem?.associatedMedia?.coverUrl!!), 0, player.currentMediaItem?.associatedMedia?.url!!, cookie) + } + + override fun onRouteSelected(router: MediaRouter, route: MediaRouter.RouteInfo) { + super.onRouteSelected(router, route) + Log.d(TAG, "#NATIVE LOGS ==> CAST: Route selected: " + route.getName()) + } + + override fun onRouteUnselected( + router: MediaRouter, + route: MediaRouter.RouteInfo, + reason: Int + ) { + super.onRouteUnselected(router, route, reason) + Log.d( + TAG, + "#NATIVE LOGS ==> CAST: Route unselected: " + route.getName() + ", reason: " + reason + ) + } + } + + val selector: MediaRouteSelector.Builder = MediaRouteSelector.Builder() + .addControlCategory(CATEGORY_LIVE_AUDIO) + .addControlCategory(CATEGORY_LIVE_VIDEO) + .addControlCategory(CATEGORY_REMOTE_PLAYBACK) + + mediaRouter.addCallback( + selector.build(), mediaRouterCallback!!, + 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 loadMedia( + title: String, + artist: String, + image: Uri?, + playPosition: Long, + url: String, + cookie: String + ) { + val intent = Intent(context, CastContext::class.java) + + castContext?.sessionManager?.startSession(intent) + val musictrackMetaData = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK) + musictrackMetaData.putString(MediaMetadata.KEY_TITLE, title) + musictrackMetaData.putString(MediaMetadata.KEY_ARTIST, artist) + musictrackMetaData.putString(MediaMetadata.KEY_ALBUM_TITLE, "albumName") + musictrackMetaData.putString("images", image.toString()) + + image?.let { + musictrackMetaData.addImage(WebImage(it)) + } + + val mediaInfo = + MediaInfo.Builder(url).setContentUrl(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(playPosition) + .setCredentials( + cookieok + ) + .build() + + val request = + sessionManager?.currentCastSession?.remoteMediaClient?.load(mediaInfo, options) + request?.addStatusListener(this) + } + + + //CAST STATE LISTENER + override fun onCastStateChanged(state: Int) { + Log.d( + TAG, + "#NATIVE LOGS ==> CAST: RECEIVER UPDATE AVAILABLE $state ${state != CastState.NO_DEVICES_AVAILABLE}" + ) + isConnected = state == CastState.CONNECTED + } + + //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") + } + + 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 + } +} \ No newline at end of file From 9b553fc8f6118fe2da854b392f15c93f35673787 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 20 Dec 2024 14:17:32 -0300 Subject: [PATCH 03/34] example and others files --- packages/player/android/build.gradle | 8 +- .../android/src/main/AndroidManifest.xml | 3 + .../player/MediaButtonEventHandler.kt | 5 + .../br/com/suamusica/player/MediaService.kt | 15 + .../player/MediaSessionConnection.kt | 6 + .../com/suamusica/player/PackageValidator.kt | 14 +- .../br/com/suamusica/player/PlayerPlugin.kt | 5 +- .../player/example/android/app/build.gradle | 2 +- packages/player/example/lib/main.dart | 14 +- .../player/example/lib/service_discovery.dart | 48 +++ packages/player/example/lib/sm_player.dart | 407 ++++++++++-------- packages/player/lib/src/player.dart | 5 + packages/player/pubspec.yaml | 5 + 13 files changed, 327 insertions(+), 210 deletions(-) create mode 100644 packages/player/example/lib/service_discovery.dart diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index 96ffe242..c1ef57da 100644 --- a/packages/player/android/build.gradle +++ b/packages/player/android/build.gradle @@ -32,7 +32,7 @@ kapt { } android { - compileSdkVersion 34 + compileSdk = 35 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -60,8 +60,12 @@ 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' + // Add the MediaRouter library + implementation 'androidx.mediarouter:mediarouter:1.7.0' + implementation 'androidx.media3:media3-cast:1.5.1'// Use the latest stable version } 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/MediaButtonEventHandler.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt index 2a9611f2..13ab20e7 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 @@ -94,6 +94,7 @@ 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)) }.build() val playerCommands = @@ -124,6 +125,10 @@ class MediaButtonEventHandler( buildIcons() } + if(customCommand.customAction == "cast"){ + mediaService.cast(args.getString("cast_id")) + } + if(customCommand.customAction == UPDATE_IS_PLAYING){ buildIcons() } 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..b1b9b45e 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 @@ -50,11 +50,17 @@ import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService import androidx.media3.session.SessionError +import androidx.mediarouter.media.MediaRouteSelector +import androidx.mediarouter.media.MediaRouter import br.com.suamusica.player.PlayerPlugin.Companion.FALLBACK_URL import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.cookie import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory +import com.google.android.gms.cast.CastMediaControlIntent +import com.google.android.gms.cast.framework.CastContext +import com.google.android.gms.cast.framework.CastSession +import com.google.android.gms.cast.framework.SessionManagerListener import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -105,6 +111,8 @@ class MediaService : MediaSessionService() { private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private val mediaItemMediaAssociations = WeakHashMap() + //CAST + private var cast:Cast? = null override fun onCreate() { super.onCreate() mediaButtonEventHandler = MediaButtonEventHandler(this) @@ -168,6 +176,12 @@ class MediaService : MediaSessionService() { } } + fun cast(castId: String?) { + cast = Cast(this, player,cookie, mediaItemMediaAssociations) + cast?.discoveryCast() + cast?.connectToCast(castId!!) + } + fun removeNotification() { Log.d("Player", "removeNotification") player?.stop() @@ -499,6 +513,7 @@ class MediaService : MediaSessionService() { player?.clearMediaItems() } + fun seek(position: Long, playWhenReady: Boolean) { player?.seekTo(position) player?.playWhenReady = playWhenReady 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..794af4ff 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 @@ -88,6 +88,12 @@ class MediaSessionConnection( sendCommand("play", bundle) } + fun cast(id: String){ + val bundle = Bundle() + bundle.putString("cast_id", id) + sendCommand("cast", bundle) + } + fun setRepeatMode(mode: String) { val bundle = Bundle() bundle.putString("mode", mode) 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/PlayerPlugin.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt index 24cb0d5b..a1df7f92 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 @@ -146,7 +146,10 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { shouldNotifyTransition, ) } - + "cast" -> { + val id = call.argument("castId") ?: "" + PlayerSingleton.mediaSessionConnection?.cast(id) + } PLAY_METHOD -> { val shouldPrepare = call.argument("shouldPrepare") ?: false PlayerSingleton.mediaSessionConnection?.play(shouldPrepare) diff --git a/packages/player/example/android/app/build.gradle b/packages/player/example/android/app/build.gradle index 9d68c793..4becbe9e 100644 --- a/packages/player/example/android/app/build.gradle +++ b/packages/player/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 34 + compileSdkVersion 35 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/packages/player/example/lib/main.dart b/packages/player/example/lib/main.dart index 102d9078..d00817da 100644 --- a/packages/player/example/lib/main.dart +++ b/packages/player/example/lib/main.dart @@ -9,21 +9,11 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Player example app'), - ), - body: Material( - child: SMPlayer(), - ), + home: Material( + child: SMPlayer(), ), ); } diff --git a/packages/player/example/lib/service_discovery.dart b/packages/player/example/lib/service_discovery.dart new file mode 100644 index 00000000..6cc816d6 --- /dev/null +++ b/packages/player/example/lib/service_discovery.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +import 'package:mdns_plugin/mdns_plugin.dart'; + +class ServiceDiscovery { + ServiceDiscovery(Function(MDNSService)? onFound) { + _flutterMdnsPlugin = MDNSPlugin( + DelegateMDNS( + (MDNSService service) { + final fn = toUTF8String(service.txt?['fn']); + if (fn != null) { + service.map['name'] = fn; + } + onFound?.call(service); + }, + ), + ); + } + late MDNSPlugin _flutterMdnsPlugin; + void startDiscovery() => _flutterMdnsPlugin.startDiscovery( + '_googlecast._tcp', + enableUpdating: true, + ); + + void stopDiscovery() => _flutterMdnsPlugin.stopDiscovery(); +} + +String? toUTF8String(List? bytes) => + bytes == null ? null : const Utf8Codec().decode(bytes); + +class DelegateMDNS implements MDNSPluginDelegate { + DelegateMDNS(this.resolved); + final Function(MDNSService)? resolved; + @override + void onDiscoveryStarted() {} + @override + void onDiscoveryStopped() {} + @override + void onServiceUpdated(MDNSService service) {} + @override + void onServiceRemoved(MDNSService service) {} + @override + bool onServiceFound(MDNSService service) => true; + @override + void onServiceResolved(MDNSService service) { + resolved?.call(service); + } +} diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 25328700..2ca1048d 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({ @@ -327,211 +329,242 @@ 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', - ), - IconButton( - icon: Icon(Icons.add), - onPressed: () { - _player.enqueueAll( - [media1, media2, media3, media4], - autoPlay: true, - ); - }, - tooltip: 'Add media', - ), - IconButton( - icon: Icon(Icons.queue), - onPressed: () { - _player.enqueueAll( - [media1, media2, media3, media4], - ); + List foundServices = []; + + return Scaffold( + appBar: AppBar( + title: Text('SM Player'), + actions: [ + IconButton( + icon: Icon(Icons.cast), + onPressed: () { + ServiceDiscovery( + (service) { + String castId = + String.fromCharCodes(service.map['txt']['id']); + _player.cast(castId); }, - tooltip: 'Add media', - ), - IconButton( - icon: Icon(Icons.folder), - onPressed: pickLocalFile, - tooltip: 'Add local file', - ), - IconButton( - icon: Icon(Icons.play_arrow), - onPressed: () => _player.playFromQueue(1, - loadOnly: true, position: Duration(seconds: 50)), - tooltip: 'Play from queue', - ), - ], + ).startDiscovery(); + }, ), - Stack( - children: [ - Wrap( - direction: Axis.horizontal, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: EdgeInsets.only(left: 20.0), - child: Text(positionText, - style: TextStyle(fontSize: 14.0)), - ), - Padding( - padding: EdgeInsets.only(right: 20.0), - child: Text(durationText, - style: TextStyle(fontSize: 14.0)), - ) - ], - ), - ], - ), - Container( - width: double.infinity, - margin: EdgeInsets.only(top: 5.0), - child: SliderTheme( - data: SliderTheme.of(context).copyWith( - trackHeight: 2.0, - thumbShape: - const RoundSliderThumbShape(enabledThumbRadius: 7.0), - showValueIndicator: ShowValueIndicator.always, - ), - child: Slider( - activeColor: AppColors.redPink, - inactiveColor: AppColors.inactiveColor, - min: 0.0, - max: duration.inMilliseconds.toDouble(), - value: position.inMilliseconds.toDouble(), - onChanged: (double value) { - seek(value); - }, - ), + ], + ), + body: 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', ), - ), - ], - ), - 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), + IconButton( + icon: Icon(Icons.add), + onPressed: () { + _player.enqueueAll( + [media1, media2, media3, media4], + autoPlay: true, + ); + }, + tooltip: 'Add media', + ), + IconButton( + icon: Icon(Icons.queue), + onPressed: () { + _player.enqueueAll( + [media1, media2, media3, media4], + ); + }, + tooltip: 'Add media', + ), + IconButton( + icon: Icon(Icons.folder), + onPressed: pickLocalFile, + tooltip: 'Add local file', + ), + IconButton( + icon: Icon(Icons.play_arrow), + onPressed: () => _player.playFromQueue(1, + loadOnly: true, position: Duration(seconds: 50)), + tooltip: 'Play from queue', + ), + ], + ), + Stack( + 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( + ), + 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: 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}')), + onPressed: previous, + iconSize: 40, + icon: Container( + child: SvgPicture.asset(UIData.btPlayerPrevious), + ), ), - onTap: () { - _player.playFromQueue(index); - }, ), - ) - .toList(), - ), + ), + Material( + borderRadius: BorderRadius.circular(58.0), + clipBehavior: Clip.hardEdge, + child: IconButton( + iconSize: 58, + icon: _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(), + ), + ), + ) + ], + ), ), ); } diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 65b5bb14..c8fb6cc9 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -131,6 +131,11 @@ class Player { return Ok; } + Future cast(String castId) async { + await _channel.invokeMethod('cast', {'castId': castId}); + return Ok; + } + Future enqueueAll( List items, { bool autoPlay = false, diff --git a/packages/player/pubspec.yaml b/packages/player/pubspec.yaml index adf86269..387666c4 100644 --- a/packages/player/pubspec.yaml +++ b/packages/player/pubspec.yaml @@ -24,6 +24,11 @@ dependencies: git: url: https://github.com/SuaMusica/flutter_plugins.git path: packages/aws/ + mdns_plugin: + git: + url: https://github.com/SuaMusica/flutter + path: mdns_plugin + ref: develop dev_dependencies: build_runner: ^2.2.1 From cf904d48830f8f9959bb008eb6273bcdd082f02b Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 23 Dec 2024 19:11:52 -0300 Subject: [PATCH 04/34] controls ok --- .../kotlin/br/com/suamusica/player/Cast.kt | 168 +++++--- .../suamusica/player/CastOptionsProvider.kt | 45 +++ .../player/MediaButtonEventHandler.kt | 7 +- .../br/com/suamusica/player/MediaService.kt | 381 ++++++------------ .../suamusica/player/PlayerChangeNotifier.kt | 2 +- .../com/suamusica/player/PlayerExtensions.kt | 33 ++ .../br/com/suamusica/player/PlayerPlugin.kt | 16 +- .../com/suamusica/player/PlayerSingleton.kt | 5 + .../br/com/suamusica/player/PlayerSwitcher.kt | 294 ++++++++++++++ packages/player/example/lib/sm_player.dart | 2 +- packages/player/lib/src/player.dart | 2 +- 11 files changed, 631 insertions(+), 324 deletions(-) create mode 100644 packages/player/android/src/main/kotlin/br/com/suamusica/player/CastOptionsProvider.kt create mode 100644 packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerExtensions.kt create mode 100644 packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSwitcher.kt 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 index 3c9bafdf..ec17ed9e 100644 --- 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 @@ -1,8 +1,6 @@ package br.com.suamusica.player -import android.R import android.content.Context -import android.content.Intent import android.media.MediaRouter.RouteInfo.DEVICE_TYPE_TV import android.net.Uri import android.util.Log @@ -16,30 +14,31 @@ 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 br.com.suamusica.player.PlayerPlugin.Companion.cookie import com.google.android.gms.cast.* -import com.google.android.gms.cast.Cast import com.google.android.gms.cast.framework.CastContext +import com.google.android.gms.cast.framework.CastSession 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.cast.framework.media.RemoteMediaClient import com.google.android.gms.common.api.PendingResult import com.google.android.gms.common.api.Status import com.google.android.gms.common.images.WebImage +import org.json.JSONException +import org.json.JSONObject import java.util.WeakHashMap -import java.util.concurrent.Executors @UnstableApi -class Cast( + +class CastManager( + private val castContext: CastContext, private val context: Context, private val player: ExoPlayer? = null, - private val cookie: String = "", private val mediaItemMediaAssociations: WeakHashMap = WeakHashMap(), - - ) : +) : SessionAvailabilityListener, CastStateListener, Cast.Listener(), @@ -47,21 +46,20 @@ class Cast( PendingResult.StatusListener { companion object { const val TAG = "Chromecast" - - } private var mediaRouter = MediaRouter.getInstance(context) - private var isConnected = false - private var castContext: CastContext? = null + 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 = CastContext.getSharedInstance(context) - castContext?.addCastStateListener(this) - sessionManager = castContext?.sessionManager - + castContext.addCastStateListener(this) + sessionManager = castContext.sessionManager mediaRouterCallback = object : MediaRouter.Callback() { override fun onRouteAdded(router: MediaRouter, route: MediaRouter.RouteInfo) { super.onRouteAdded(router, route) @@ -76,13 +74,18 @@ class Cast( override fun onRouteChanged(router: MediaRouter, route: MediaRouter.RouteInfo) { super.onRouteChanged(router, route) Log.d(TAG, "#NATIVE LOGS ==> CAST: Route changed: " + route.getName()) - player?.seekTo(3,0) - loadMedia(player?.currentMediaItem?.associatedMedia?.name!!, "Artist", Uri.parse(player.currentMediaItem?.associatedMedia?.coverUrl!!), 0, player.currentMediaItem?.associatedMedia?.url!!, cookie) } - override fun onRouteSelected(router: MediaRouter, route: MediaRouter.RouteInfo) { - super.onRouteSelected(router, route) - Log.d(TAG, "#NATIVE LOGS ==> CAST: Route selected: " + route.getName()) + override fun onRouteSelected( + router: MediaRouter, + route: MediaRouter.RouteInfo, + reason: Int + ) { + super.onRouteSelected(router, route, reason) + Log.d( + TAG, + "#NATIVE LOGS ==> CAST: Route selected: " + route.getName() + ", reason: " + reason + ) } override fun onRouteUnselected( @@ -111,7 +114,7 @@ class Cast( fun discoveryCast(): List> { val casts = mutableListOf>() - if (castContext?.castState != CastState.NO_DEVICES_AVAILABLE) { + if (castContext.castState != CastState.NO_DEVICES_AVAILABLE) { mediaRouter.routes.forEach { if (it.deviceType == DEVICE_TYPE_TV && it.id.isNotEmpty()) { casts.add( @@ -126,7 +129,8 @@ class Cast( return casts } - fun connectToCast(idCast: String) { + fun connectToCast(idCast: String, cookie: String) { + this.cookie = cookie val item = mediaRouter.routes.firstOrNull { it.id.contains(idCast) } @@ -140,41 +144,64 @@ class Cast( } } - fun loadMedia( - title: String, - artist: String, - image: Uri?, - playPosition: Long, - url: String, - cookie: String - ) { - val intent = Intent(context, CastContext::class.java) + 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!! - castContext?.sessionManager?.startSession(intent) val musictrackMetaData = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK) - musictrackMetaData.putString(MediaMetadata.KEY_TITLE, title) - musictrackMetaData.putString(MediaMetadata.KEY_ARTIST, artist) + 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", image.toString()) + musictrackMetaData.putString("images", url) - image?.let { - musictrackMetaData.addImage(WebImage(it)) + media.associatedMedia?.coverUrl?.let { + musictrackMetaData.addImage(WebImage(Uri.parse(it))) } val mediaInfo = - MediaInfo.Builder(url).setContentUrl(url) + 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\": \"") + 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(playPosition) +// .setPlayPosition(player.currentPosition) .setCredentials( - cookieok + cookieOk ) .build() @@ -183,14 +210,56 @@ class Cast( 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 $state ${state != CastState.NO_DEVICES_AVAILABLE}" + "#NATIVE LOGS ==> CAST: RECEIVER UPDATE AVAILABLE ${CastState.toString(state)}" ) - isConnected = state == CastState.CONNECTED + + if (alreadyConnected && state == CastState.NOT_CONNECTED) { + alreadyConnected = false + } + + if (!alreadyConnected) { + isConnected = state == CastState.CONNECTED + if (isConnected) { + alreadyConnected = true + onConnectCallback?.invoke() + } + } } //SessionAvailabilityListener @@ -235,6 +304,7 @@ class Cast( override fun onSessionStarted(p0: Session, p1: String) { Log.d(TAG, "#NATIVE LOGS ==> CAST: onCastSessionUnavailable") + onSessionEndedCallback?.invoke() } override fun onSessionStarting(p0: Session) { @@ -245,9 +315,17 @@ class Cast( 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..1ff7fd4d --- /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 13ab20e7..45c0a3f4 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 @@ -126,7 +126,8 @@ class MediaButtonEventHandler( } if(customCommand.customAction == "cast"){ - mediaService.cast(args.getString("cast_id")) +// mediaService.cast(args.getString("cast_id")) + mediaService.castWithCastPlayer(args.getString("cast_id")) } if(customCommand.customAction == UPDATE_IS_PLAYING){ @@ -207,10 +208,10 @@ class MediaButtonEventHandler( }else{ session.player.seekToPrevious() } - mediaService.shouldNotifyTransition = true + PlayerSingleton.shouldNotifyTransition = true } if (customCommand.customAction == "notification_next" || customCommand.customAction == "next") { - mediaService.shouldNotifyTransition = true + PlayerSingleton.shouldNotifyTransition = true session.player.seekToNextMediaItem() } if (customCommand.customAction == "pause") { diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt index b1b9b45e..f6de2fd0 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,7 +1,7 @@ package br.com.suamusica.player +import PlayerSwitcher import android.app.ActivityManager -import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.Intent @@ -10,7 +10,8 @@ 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.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem @@ -19,13 +20,11 @@ import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK -import androidx.media3.common.Player.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,31 +37,20 @@ 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 androidx.mediarouter.media.MediaRouteSelector -import androidx.mediarouter.media.MediaRouter import br.com.suamusica.player.PlayerPlugin.Companion.FALLBACK_URL import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.cookie import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory -import com.google.android.gms.cast.CastMediaControlIntent import com.google.android.gms.cast.framework.CastContext -import com.google.android.gms.cast.framework.CastSession -import com.google.android.gms.cast.framework.SessionManagerListener 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 @@ -72,7 +60,6 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import java.io.File import java.util.Collections -import java.util.WeakHashMap import java.util.concurrent.atomic.AtomicBoolean const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" @@ -93,34 +80,33 @@ class MediaService : MediaSessionService() { AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA) .build() - var player: ExoPlayer? = null - - private var progressTracker: ProgressTracker? = null + var player: PlayerSwitcher? = null + var exoPlayer: ExoPlayer? = 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 enqueueLoadOnly: Boolean = false + 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:Cast? = null + private var cast: CastManager? = null + private var castContext: CastContext? = null override fun onCreate() { super.onCreate() mediaButtonEventHandler = MediaButtonEventHandler(this) - - - player = ExoPlayer.Builder(this).build().apply { + castContext = CastContext.getSharedInstance(this) + exoPlayer = ExoPlayer.Builder(this).build().apply { setAudioAttributes(uAmpAudioAttributes, true) - addListener(playerEventListener()) +// addListener(playerEventListener()) setWakeMode(C.WAKE_MODE_NETWORK) setHandleAudioBecomingNoisy(true) preloadConfiguration = ExoPlayer.PreloadConfiguration( @@ -131,7 +117,7 @@ class MediaService : MediaSessionService() { dataSourceBitmapLoader = DataSourceBitmapLoader(applicationContext) - player?.let { + exoPlayer?.let { mediaSession = MediaSession.Builder(this, it) .setBitmapLoader(CacheBitmapLoader(dataSourceBitmapLoader)) .setCallback(mediaButtonEventHandler) @@ -174,17 +160,61 @@ class MediaService : MediaSessionService() { } }) } + player = PlayerSwitcher(exoPlayer!!, mediaButtonEventHandler , applicationContext) + castContext?.let { + cast = CastManager(it, this, exoPlayer!!, mediaItemMediaAssociations) + } } fun cast(castId: String?) { - cast = Cast(this, player,cookie, mediaItemMediaAssociations) - cast?.discoveryCast() - cast?.connectToCast(castId!!) + cast?.connectToCast(castId!!, cookie) + cast?.setOnConnectCallback { +// player?.pause() + Log.d(CastManager.TAG, "#NATIVE LOGS ==> CAST: OnConnectCallback") + val mediaItems = player!!.getAllMediaItems() + cast?.queueLoadCast(mediaItems) +// cast?.loadMediaOld() + } + cast?.setOnSessionEndedCallback { +// cast?.remoteMediaClient.currentItem. + player?.wrappedPlayer?.play() + } + } + + fun castWithCastPlayer(castId: String?) { + if(cast?.isConnected == true) { + return + } + val items = player!!.getAllMediaItems() + cast?.connectToCast(castId!!, cookie) + var castPlayer : CastPlayer? = null + cast?.setOnConnectCallback { + castPlayer = CastPlayer(castContext!!, DefaultMediaItemConverter()) + mediaSession.player = castPlayer!! + player?.setCurrentPlayer(castPlayer!!) + player?.wrappedPlayer?.setMediaItems(items) + player?.wrappedPlayer?.prepare() + player?.wrappedPlayer?.play() + Log.d(CastManager.TAG, "#NATIVE LOGS ==> CAST: mediaItemCounts ${castPlayer?.mediaItemCount}") + Log.d(CastManager.TAG, "#NATIVE LOGS ==> CAST: IS CAST ${player!!.wrappedPlayer is CastPlayer}") + } + + cast?.setOnSessionEndedCallback { + val currentPosition: Long = player?.wrappedPlayer?.currentPosition!! + val currentMediaItem: MediaItem = player?.wrappedPlayer?.currentMediaItem!! + + player?.wrappedPlayer!!.setMediaItem(currentMediaItem) + player?.wrappedPlayer!!.seekTo(currentPosition) + player?.wrappedPlayer!!.play() + mediaSession.player = player!! + player?.setCurrentPlayer(exoPlayer!!) + } + } fun removeNotification() { Log.d("Player", "removeNotification") - player?.stop() + player?.wrappedPlayer?.stop() // NotificationManagerCompat.from(applicationContext).cancelAll() } @@ -213,10 +243,9 @@ class MediaService : MediaSessionService() { ): MediaSession = mediaSession override fun onTaskRemoved(rootIntent: Intent?) { - player?.clearMediaItems() + player?.wrappedPlayer?.clearMediaItems() Log.d(TAG, "onTaskRemoved") - player?.stop() - stopTrackingProgress() + player?.wrappedPlayer?.stop() stopSelf() super.onTaskRemoved(rootIntent) } @@ -236,7 +265,7 @@ class MediaService : MediaSessionService() { } private fun releasePossibleLeaks() { - player?.release() + player?.wrappedPlayer?.release() mediaSession.release() mediaController = null } @@ -253,9 +282,9 @@ class MediaService : MediaSessionService() { fun updateMediaUri(index: Int, uri: String?) { // if (index != player?.currentMediaItemIndex) { - val media = player?.getMediaItemAt(index) + val media = player?.wrappedPlayer?.getMediaItemAt(index) media?.associatedMedia?.let { - player?.removeMediaItem(index) + player?.wrappedPlayer?.removeMediaItem(index) player?.addMediaSource( index, prepare( cookie, @@ -269,20 +298,20 @@ class MediaService : MediaSessionService() { } fun toggleShuffle(positionsList: List>) { - player?.shuffleModeEnabled = !(player?.shuffleModeEnabled ?: false) - player?.shuffleModeEnabled?.let { + player?.wrappedPlayer?.shuffleModeEnabled = !(player?.shuffleModeEnabled ?: false) + player?.wrappedPlayer?.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 - shuffleOrder is null: ${shuffleOrder == null} | shuffledIndices: ${PlayerSingleton.shuffledIndices.size} - ${player?.mediaItemCount}" ) player!!.setShuffleOrder(shuffleOrder!!) } @@ -316,9 +345,9 @@ class MediaService : MediaSessionService() { "enqueue: mediaItemCount: ${player?.mediaItemCount} | autoPlay: $autoPlay" ) this.autoPlay = autoPlay - this.shouldNotifyTransition = shouldNotifyTransition - if (player?.mediaItemCount == 0) { - player?.playWhenReady = autoPlay + PlayerSingleton.shouldNotifyTransition = shouldNotifyTransition + if (player?.wrappedPlayer?.mediaItemCount == 0) { + player?.wrappedPlayer?.playWhenReady = autoPlay } // enqueueLoadOnly = autoPlay android.util.Log.d( @@ -335,14 +364,14 @@ class MediaService : MediaSessionService() { mediaSources.add(prepare(cookie, medias[i], "")) } player?.addMediaSources(mediaSources) - player?.prepare() + player?.wrappedPlayer?.prepare() // PlayerSingleton.playerChangeNotifier?.notifyItemTransition("createMediaSource") // playerChangeNotifier?.currentMediaIndex( // currentIndex(), // "createMediaSource", // ) } - if(shouldNotifyTransition){ + if (PlayerSingleton.shouldNotifyTransition) { playerChangeNotifier?.notifyItemTransition("Enqueue - createMediaSource") } } @@ -361,12 +390,14 @@ class MediaService : MediaSessionService() { } else { Uri.parse(urlToPrepare) } - val mediaItem = MediaItem.Builder().setUri(uri).setMediaMetadata(metadata) - .setMediaId(media.id.toString()).build() + val mediaItem = MediaItem.Builder() + .setUri(uri) + .setMediaMetadata(metadata) + .setMediaId(media.id.toString()) + .setMimeType("audio/mpeg") + .build() mediaItem.associatedMedia = media - @C.ContentType val type = Util.inferContentType(uri) - - return when (type) { + return when (@C.ContentType val type = Util.inferContentType(uri)) { C.CONTENT_TYPE_HLS -> { HlsMediaSource.Factory(dataSourceFactory) .setPlaylistParserFactory(SMHlsPlaylistParserFactory()) @@ -397,7 +428,7 @@ class MediaService : MediaSessionService() { positionsList: List> ) { if (player?.shuffleModeEnabled == true) { - val list = shuffledIndices.ifEmpty { + val list = PlayerSingleton.shuffledIndices.ifEmpty { positionsList.map { it["originalPosition"] ?: 0 }.toMutableList() } Collections.swap(list, oldIndex, newIndex) @@ -405,7 +436,7 @@ class MediaService : MediaSessionService() { DefaultShuffleOrder(list.toIntArray(), System.currentTimeMillis()) player?.setShuffleOrder(shuffleOrder!!) } else { - player?.moveMediaItem(oldIndex, newIndex) + player?.wrappedPlayer?.moveMediaItem(oldIndex, newIndex) } } @@ -414,10 +445,10 @@ class MediaService : MediaSessionService() { if (sortedIndexes.isNotEmpty()) { sortedIndexes.forEach { player?.removeMediaItem(it) - if (shuffledIndices.isNotEmpty()) { - shuffledIndices.removeAt( - shuffledIndices.indexOf( - player?.currentMediaItemIndex ?: 0 + if (PlayerSingleton.shuffledIndices.isNotEmpty()) { + PlayerSingleton.shuffledIndices.removeAt( + PlayerSingleton.shuffledIndices.indexOf( + player?.wrappedPlayer?.currentMediaItemIndex ?: 0 ) ) } @@ -425,7 +456,7 @@ class MediaService : MediaSessionService() { } if (player?.shuffleModeEnabled == true) { shuffleOrder = DefaultShuffleOrder( - shuffledIndices.toIntArray(), + PlayerSingleton.shuffledIndices.toIntArray(), System.currentTimeMillis() ) player?.setShuffleOrder(shuffleOrder!!) @@ -433,11 +464,11 @@ class MediaService : MediaSessionService() { } fun disableRepeatMode() { - player?.repeatMode = REPEAT_MODE_OFF + player?.wrappedPlayer?.repeatMode = REPEAT_MODE_OFF } fun repeatMode() { - player?.let { + player?.wrappedPlayer?.let { when (it.repeatMode) { REPEAT_MODE_OFF -> { it.repeatMode = REPEAT_MODE_ALL @@ -463,6 +494,7 @@ class MediaService : MediaSessionService() { 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) @@ -474,16 +506,16 @@ class MediaService : MediaSessionService() { } fun play(shouldPrepare: Boolean = false) { - performAndEnableTracking { +// PlayerSingleton.performAndEnableTracking { if (shouldPrepare) { - player?.prepare() + player?.wrappedPlayer?.prepare() } - player?.play() - } + player?.wrappedPlayer?.play() +// } } fun setRepeatMode(mode: String) { - player?.repeatMode = when (mode) { + player?.wrappedPlayer?.repeatMode = when (mode) { "off" -> REPEAT_MODE_OFF "one" -> REPEAT_MODE_ONE "all" -> REPEAT_MODE_ALL @@ -492,47 +524,43 @@ class MediaService : MediaSessionService() { } fun playFromQueue(position: Int, timePosition: Long, loadOnly: Boolean = false) { - player?.playWhenReady = !loadOnly + player?.wrappedPlayer?.playWhenReady = !loadOnly if (loadOnly) { seekToLoadOnly = true } - player?.seekTo( - if (player?.shuffleModeEnabled == true) shuffledIndices[position] else position, + player?.wrappedPlayer?.seekTo( + if (player?.wrappedPlayer?.shuffleModeEnabled == true) PlayerSingleton.shuffledIndices[position] else position, timePosition, ) if (!loadOnly) { - player?.prepare() + player?.wrappedPlayer?.prepare() playerChangeNotifier?.notifyItemTransition("playFromQueue") } } fun removeAll() { - player?.stop() - player?.clearMediaItems() + player?.wrappedPlayer?.stop() + player?.wrappedPlayer?.clearMediaItems() } fun seek(position: Long, playWhenReady: Boolean) { - player?.seekTo(position) - player?.playWhenReady = playWhenReady + player?.wrappedPlayer?.seekTo(position) + player?.wrappedPlayer?.playWhenReady = playWhenReady } fun pause() { - performAndDisableTracking { - player?.pause() - } + player?.wrappedPlayer?.pause() } fun stop() { - performAndDisableTracking { - player?.stop() - } + player?.wrappedPlayer?.stop() } fun togglePlayPause() { - if (player?.isPlaying == true) { + if (player?.wrappedPlayer?.isPlaying == true) { pause() } else { play() @@ -540,190 +568,7 @@ class MediaService : MediaSessionService() { } 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 + player?.wrappedPlayer?.stop() } - 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() - } - } } 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..b77c58a7 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 @@ -49,7 +49,7 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { channelManager.currentMediaIndex("sua-musica-player", currentMediaIndex) } fun notifyPositionChange(position: Long, duration: Long) { - Log.i("Player", "Notifying Player Position change: position: $position duration: $duration") + // Log.i("Player", "Notifying Player Position change: position: $position duration: $duration") channelManager.notifyPositionChange("sua-musica-player", position, duration) } fun onRepeatChanged(repeatMode: Int) { 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..987c11f5 --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerExtensions.kt @@ -0,0 +1,33 @@ +package br.com.suamusica.player + +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import com.google.android.gms.cast.MediaQueueItem +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 + } + +fun Player.buildMediaQueueItems(convert: (MediaItem) -> MediaQueueItem): List { + val items = mutableListOf() + for (i in 0 until mediaItemCount) { + getMediaItemAt(i).let { mediaItem -> + items.add(convert(mediaItem)) + } + } + return items +} \ 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 a1df7f92..3dccdf47 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 @@ -89,10 +90,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){ - alreadyAttachedToActivity = true -// } + // val isStopped = + // (binding.activity.applicationInfo.flags and ApplicationInfo.FLAG_STOPPED) == ApplicationInfo.FLAG_STOPPED + // if (!isStopped) { + alreadyAttachedToActivity = true + // } } override fun onDetachedFromActivityForConfigChanges() { @@ -213,7 +215,11 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { val idFavorite = call.argument(ID_FAVORITE_ARGUMENT) ?: 0 PlayerSingleton.mediaSessionConnection?.updateFavorite(isFavorite, idFavorite) } else { - PlayerSingleton.mediaSessionConnection?.updatePlayState(call.argument(IS_PLAYING_ARGUMENT) ?: false) + PlayerSingleton.mediaSessionConnection?.updatePlayState( + call.argument( + IS_PLAYING_ARGUMENT + ) ?: false + ) } } 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..c6276fcc 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 @@ -11,6 +11,11 @@ object PlayerSingleton { private const val TAG = "Player" var playerChangeNotifier: PlayerChangeNotifier? = null + var shouldNotifyTransition: Boolean = true + + + var shuffledIndices = mutableListOf() + fun setChannel(c: MethodChannel, context: Context) { channel = c playerChangeNotifier = PlayerChangeNotifier(MethodChannelManager(c)) 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..a2c08de6 --- /dev/null +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSwitcher.kt @@ -0,0 +1,294 @@ +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 + + +@UnstableApi +class PlayerSwitcher(private var currentPlayer: Player, private var mediaButtonEventHandler: MediaButtonEventHandler, private var context: Context) : ForwardingPlayer(currentPlayer) { + private var playerEventListener: Player.Listener? = null + private val TAG = "PlayerSwitcher" + private var progressTracker: ProgressTracker? = null + + init { + playerEventListener?.let { currentPlayer.removeListener(it) } + setupPlayerListener() + } + + fun setCurrentPlayer(newPlayer: Player) { + if (this.currentPlayer === newPlayer) { + return + } + + // Salva o estado atual do player + val playerState = capturePlayerState() + + // Remove listener do player anterior + playerEventListener?.let { currentPlayer.removeListener(it) } + + // Para e limpa o player anterior + stopAndClearCurrentPlayer() + + // Atualiza para o novo player + this.currentPlayer = newPlayer + + // Restaura o estado no novo player + restorePlayerState(playerState) + + // Adiciona listener no novo player + setupPlayerListener() + } + + private data class PlayerState( + val playbackPositionMs: Long = C.TIME_UNSET, + val currentItemIndex: Int = C.INDEX_UNSET, + val playWhenReady: Boolean = false, + val mediaItems: List = emptyList() + ) + + private fun capturePlayerState(): PlayerState { + return PlayerState( + playbackPositionMs = if (currentPlayer.playbackState != STATE_ENDED) currentPlayer.currentPosition else C.TIME_UNSET, + currentItemIndex = currentPlayer.currentMediaItemIndex, + playWhenReady = currentPlayer.playWhenReady, + mediaItems = currentPlayer.getAllMediaItems() + ) + } + + private fun stopAndClearCurrentPlayer() { + currentPlayer.stop() + currentPlayer.clearMediaItems() + } + + private fun restorePlayerState(state: PlayerState) { + currentPlayer.setMediaItems( + state.mediaItems, + state.currentItemIndex, + state.playbackPositionMs + ) + currentPlayer.playWhenReady = state.playWhenReady + currentPlayer.prepare() + } + + private fun setupPlayerListener() { + playerEventListener = object : Player.Listener { + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + if (reason == DISCONTINUITY_REASON_SEEK) { + playerChangeNotifier?.notifySeekEnd() + } + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + playerChangeNotifier?.notifyPlaying(isPlaying) + if (isPlaying) { + startTrackingProgress() + } else { + stopTrackingProgress() + } + } + + override fun onMediaItemTransition( + mediaItem: MediaItem?, + reason: @Player.MediaItemTransitionReason Int + ) { + super.onMediaItemTransition(mediaItem, reason) + Log.d(TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason") + if ((currentPlayer.mediaItemCount ?: 0) > 0) { + playerChangeNotifier?.currentMediaIndex( + currentIndex(), + "onMediaItemTransition", + ) + } + mediaButtonEventHandler.buildIcons() + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED || !PlayerSingleton.shouldNotifyTransition) { + return + } + playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: ${reason} | shouldNotifyTransition: ${PlayerSingleton.shouldNotifyTransition}") + PlayerSingleton.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 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 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 { + val position = if (currentPlayer.shuffleModeEnabled == true) { + PlayerSingleton.shuffledIndices.indexOf( + currentPlayer.currentMediaItemIndex ?: 0 + ) + } else { + currentPlayer.currentMediaItemIndex ?: 0 + } + return position + } + + 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 ?: 0L + 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/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 2ca1048d..ff5bbd11 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -48,7 +48,7 @@ var media3 = Media( 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, diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index c8fb6cc9..5c3edd52 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -470,7 +470,7 @@ class Player { static Future _doHandlePlatformCall(MethodCall call) async { final currentMedia = _queue.current; final currentIndex = _queue.index; - print('call.arguments: ${call.arguments}'); + // print('call.arguments: ${call.arguments}'); final Map callArgs = call.arguments as Map; if (call.method != 'audio.onCurrentPosition') { _log('_platformCallHandler call ${call.method} $callArgs'); From ac52aaf798c0c8afe609edfa203f12f7a015165e Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 23 Dec 2024 19:12:11 -0300 Subject: [PATCH 05/34] remove empty spaces --- .../src/main/kotlin/br/com/suamusica/player/MediaService.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt index f6de2fd0..2b88204e 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 @@ -90,10 +90,8 @@ class MediaService : MediaSessionService() { private var seekToLoadOnly: Boolean = false // private var enqueueLoadOnly: Boolean = false - private var autoPlay: Boolean = true - private val channel = Channel>(Channel.BUFFERED) private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) From 7ff585aa07b77564aacc408d1829a5a2db6ad794 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 23 Dec 2024 22:30:03 -0300 Subject: [PATCH 06/34] working --- .../kotlin/br/com/suamusica/player/Cast.kt | 9 ++++++++ .../br/com/suamusica/player/MediaService.kt | 22 +++++++++---------- .../br/com/suamusica/player/PlayerSwitcher.kt | 15 ++++++++++--- 3 files changed, 31 insertions(+), 15 deletions(-) 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 index ec17ed9e..15a609ba 100644 --- 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 @@ -14,6 +14,7 @@ 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.MediaRouter.UNSELECT_REASON_STOPPED import com.google.android.gms.cast.* import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastSession @@ -144,6 +145,14 @@ class CastManager( } } + 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() 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 2b88204e..8eae800d 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 @@ -181,39 +181,37 @@ class MediaService : MediaSessionService() { fun castWithCastPlayer(castId: String?) { if(cast?.isConnected == true) { + cast?.disconnect() return } val items = player!!.getAllMediaItems() cast?.connectToCast(castId!!, cookie) var castPlayer : CastPlayer? = null cast?.setOnConnectCallback { + val index = player?.wrappedPlayer?.currentMediaItemIndex ?: 0 + val currentPosition: Long = player?.wrappedPlayer?.currentPosition!! castPlayer = CastPlayer(castContext!!, DefaultMediaItemConverter()) mediaSession.player = castPlayer!! player?.setCurrentPlayer(castPlayer!!) - player?.wrappedPlayer?.setMediaItems(items) + //o seek to foi mais rapido, testar + player?.wrappedPlayer?.setMediaItems(items,index,currentPosition) player?.wrappedPlayer?.prepare() player?.wrappedPlayer?.play() - Log.d(CastManager.TAG, "#NATIVE LOGS ==> CAST: mediaItemCounts ${castPlayer?.mediaItemCount}") - Log.d(CastManager.TAG, "#NATIVE LOGS ==> CAST: IS CAST ${player!!.wrappedPlayer is CastPlayer}") } cast?.setOnSessionEndedCallback { - val currentPosition: Long = player?.wrappedPlayer?.currentPosition!! - val currentMediaItem: MediaItem = player?.wrappedPlayer?.currentMediaItem!! - - player?.wrappedPlayer!!.setMediaItem(currentMediaItem) - player?.wrappedPlayer!!.seekTo(currentPosition) - player?.wrappedPlayer!!.play() - mediaSession.player = player!! + val currentPosition = player?.wrappedPlayer?.currentPosition!! + val index = player?.wrappedPlayer?.currentMediaItemIndex!! + mediaSession.player = exoPlayer!! player?.setCurrentPlayer(exoPlayer!!) + player?.wrappedPlayer?.prepare() + player?.wrappedPlayer?.seekTo(index,currentPosition) } - } fun removeNotification() { Log.d("Player", "removeNotification") player?.wrappedPlayer?.stop() -// NotificationManagerCompat.from(applicationContext).cancelAll() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 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 index a2c08de6..b4d2d585 100644 --- 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 @@ -20,10 +20,15 @@ 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 @UnstableApi -class PlayerSwitcher(private var currentPlayer: Player, private var mediaButtonEventHandler: MediaButtonEventHandler, private var context: Context) : ForwardingPlayer(currentPlayer) { +class PlayerSwitcher( + private var currentPlayer: Player, + private var mediaButtonEventHandler: MediaButtonEventHandler, + private var context: Context +) : ForwardingPlayer(currentPlayer) { private var playerEventListener: Player.Listener? = null private val TAG = "PlayerSwitcher" private var progressTracker: ProgressTracker? = null @@ -51,7 +56,9 @@ class PlayerSwitcher(private var currentPlayer: Player, private var mediaButtonE this.currentPlayer = newPlayer // Restaura o estado no novo player - restorePlayerState(playerState) + if(currentPlayer is CastPlayer) { + restorePlayerState(playerState) + } // Adiciona listener no novo player setupPlayerListener() @@ -75,7 +82,9 @@ class PlayerSwitcher(private var currentPlayer: Player, private var mediaButtonE private fun stopAndClearCurrentPlayer() { currentPlayer.stop() - currentPlayer.clearMediaItems() + if (currentPlayer is CastPlayer) { + currentPlayer.clearMediaItems() + } } private fun restorePlayerState(state: PlayerState) { From 6f6e1b5d58b1232c99b82c2fc2a214314fd403de Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 14 Jan 2025 17:14:19 -0300 Subject: [PATCH 07/34] fix pause when buffering and wrap player --- packages/player/android/build.gradle | 9 +- .../kotlin/br/com/suamusica/player/Cast.kt | 277 +++++++++--------- .../suamusica/player/CastOptionsProvider.kt | 26 +- .../player/MediaButtonEventHandler.kt | 3 +- .../br/com/suamusica/player/MediaService.kt | 240 +++++++++------ .../player/MediaSessionConnection.kt | 6 +- .../suamusica/player/PlayerChangeNotifier.kt | 2 +- .../com/suamusica/player/PlayerExtensions.kt | 13 +- .../br/com/suamusica/player/PlayerPlugin.kt | 19 +- .../com/suamusica/player/PlayerSingleton.kt | 14 +- .../br/com/suamusica/player/PlayerState.kt | 1 + .../br/com/suamusica/player/PlayerSwitcher.kt | 60 ++-- packages/player/example/lib/sm_player.dart | 4 +- packages/player/lib/src/player.dart | 39 ++- packages/player/lib/src/player_state.dart | 2 +- 15 files changed, 362 insertions(+), 353 deletions(-) diff --git a/packages/player/android/build.gradle b/packages/player/android/build.gradle index c1ef57da..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() @@ -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 @@ -64,8 +64,7 @@ dependencies { implementation "com.google.code.gson:gson:2.10.1" // api 'com.google.android.gms:play-services-cast-framework:22.0.0' - // Add the MediaRouter library + // CHROMECAST DEPENDENCIES implementation 'androidx.mediarouter:mediarouter:1.7.0' - implementation 'androidx.media3:media3-cast:1.5.1'// Use the latest stable version - + implementation 'androidx.media3:media3-cast:1.5.1' } 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 index 15a609ba..458c8943 100644 --- 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 @@ -1,44 +1,33 @@ package br.com.suamusica.player import android.content.Context -import android.media.MediaRouter.RouteInfo.DEVICE_TYPE_TV -import android.net.Uri import android.util.Log import androidx.media3.cast.SessionAvailabilityListener -import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer +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.MediaRouter.UNSELECT_REASON_STOPPED +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.CastSession 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.cast.framework.media.RemoteMediaClient import com.google.android.gms.common.api.PendingResult import com.google.android.gms.common.api.Status -import com.google.android.gms.common.images.WebImage -import org.json.JSONException -import org.json.JSONObject -import java.util.WeakHashMap @UnstableApi class CastManager( - private val castContext: CastContext, - private val context: Context, - private val player: ExoPlayer? = null, - private val mediaItemMediaAssociations: WeakHashMap = WeakHashMap(), + castContext: CastContext, + context: Context, ) : SessionAvailabilityListener, CastStateListener, @@ -61,6 +50,8 @@ class CastManager( 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) @@ -82,7 +73,6 @@ class CastManager( route: MediaRouter.RouteInfo, reason: Int ) { - super.onRouteSelected(router, route, reason) Log.d( TAG, "#NATIVE LOGS ==> CAST: Route selected: " + route.getName() + ", reason: " + reason @@ -94,7 +84,6 @@ class CastManager( route: MediaRouter.RouteInfo, reason: Int ) { - super.onRouteUnselected(router, route, reason) Log.d( TAG, "#NATIVE LOGS ==> CAST: Route unselected: " + route.getName() + ", reason: " + reason @@ -103,35 +92,34 @@ class CastManager( } val selector: MediaRouteSelector.Builder = MediaRouteSelector.Builder() - .addControlCategory(CATEGORY_LIVE_AUDIO) - .addControlCategory(CATEGORY_LIVE_VIDEO) .addControlCategory(CATEGORY_REMOTE_PLAYBACK) - - mediaRouter.addCallback( - selector.build(), mediaRouterCallback!!, - 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, - ) - ) - } - } + //TODO: remover? + mediaRouterCallback?.let { + mediaRouter.addCallback( + selector.build(), it, + MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN + ) } - return casts } - fun connectToCast(idCast: String, cookie: String) { - this.cookie = cookie +// 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) } @@ -153,103 +141,103 @@ class CastManager( } } - 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() - } +// 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) { @@ -325,15 +313,16 @@ class CastManager( Log.d(TAG, "#NATIVE LOGS ==> CAST: onSessionSuspended") } - var MediaItem.associatedMedia: Media? - get() = mediaItemMediaAssociations[this] - set(value) { - mediaItemMediaAssociations[this] = value - } +// var MediaItem.associatedMedia: Media? +// get() = mediaItemMediaAssociations[this] +// set(value) { +// mediaItemMediaAssociations[this] = value +// } fun setOnConnectCallback(callback: () -> Unit) { onConnectCallback = callback } + fun setOnSessionEndedCallback(callback: () -> Unit) { onSessionEndedCallback = callback } 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 index 1ff7fd4d..eb6e9429 100644 --- 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 @@ -14,21 +14,21 @@ 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 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 compatButtonAction = intArrayOf(1, 3) +// val notificationOptions = +// NotificationOptions.Builder() +// .setActions(buttonActions, compatButtonAction) +// .setSkipStepMs(30000) +// .build() val mediaOptions = CastMediaOptions.Builder() - .setNotificationOptions(notificationOptions) +// .setNotificationOptions(notificationOptions) .setMediaSessionEnabled(false) .setNotificationOptions(null) .build() 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 45c0a3f4..6ce2bbbd 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 @@ -187,8 +187,7 @@ class MediaButtonEventHandler( mediaService.stop() } if (customCommand.customAction == "play") { - val shouldPrepare = args.getBoolean("shouldPrepare") - mediaService.play(shouldPrepare) + mediaService.play() } if (customCommand.customAction == SET_REPEAT_MODE) { val mode = args.getString("mode") 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 8eae800d..e1f12607 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 @@ -5,26 +5,25 @@ import android.app.ActivityManager import android.app.PendingIntent import android.app.Service import android.content.Intent +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.media.AudioManager.OnAudioFocusChangeListener 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.media.AudioFocusRequestCompat +import androidx.media.AudioManagerCompat 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.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.Player.STATE_IDLE import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util @@ -49,6 +48,7 @@ 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.ListenableFuture @@ -58,15 +58,16 @@ 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.concurrent.atomic.AtomicBoolean + const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 @UnstableApi -class MediaService : MediaSessionService() { +class MediaService : MediaSessionService(){ private val TAG = "MediaService" private val userAgent = "SuaMusica/player (Linux; Android ${Build.VERSION.SDK_INT}; ${Build.BRAND}/${Build.MODEL})" @@ -76,9 +77,10 @@ class MediaService : MediaSessionService() { lateinit var mediaSession: MediaSession private var mediaController: ListenableFuture? = null - private val uAmpAudioAttributes = - AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA) - .build() + private val uAmpAudioAttributes = AudioAttributes.Builder() + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .setUsage(C.USAGE_MEDIA) + .build() var player: PlayerSwitcher? = null var exoPlayer: ExoPlayer? = null @@ -98,13 +100,50 @@ class MediaService : MediaSessionService() { //CAST private var cast: CastManager? = null private var castContext: CastContext? = null + + private val smPlayer get() = player?.wrappedPlayer + override fun onCreate() { super.onCreate() mediaButtonEventHandler = MediaButtonEventHandler(this) 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( @@ -158,60 +197,48 @@ class MediaService : MediaSessionService() { } }) } - player = PlayerSwitcher(exoPlayer!!, mediaButtonEventHandler , applicationContext) + player = PlayerSwitcher(exoPlayer!!, mediaButtonEventHandler) castContext?.let { - cast = CastManager(it, this, exoPlayer!!, mediaItemMediaAssociations) - } - } - - fun cast(castId: String?) { - cast?.connectToCast(castId!!, cookie) - cast?.setOnConnectCallback { -// player?.pause() - Log.d(CastManager.TAG, "#NATIVE LOGS ==> CAST: OnConnectCallback") - val mediaItems = player!!.getAllMediaItems() - cast?.queueLoadCast(mediaItems) -// cast?.loadMediaOld() - } - cast?.setOnSessionEndedCallback { -// cast?.remoteMediaClient.currentItem. - player?.wrappedPlayer?.play() + cast = CastManager(it, this) } } fun castWithCastPlayer(castId: String?) { - if(cast?.isConnected == true) { + if (cast?.isConnected == true) { cast?.disconnect() return } - val items = player!!.getAllMediaItems() - cast?.connectToCast(castId!!, cookie) - var castPlayer : CastPlayer? = null + val items = player?.getAllMediaItems() + cast?.connectToCast(castId!!) + var castPlayer: CastPlayer? cast?.setOnConnectCallback { - val index = player?.wrappedPlayer?.currentMediaItemIndex ?: 0 - val currentPosition: Long = player?.wrappedPlayer?.currentPosition!! - castPlayer = CastPlayer(castContext!!, DefaultMediaItemConverter()) + val index = smPlayer?.currentMediaItemIndex ?: 0 + val currentPosition: Long = smPlayer?.currentPosition ?: 0 + castPlayer = CastPlayer(castContext!!, CustomMediaItemConverter()) mediaSession.player = castPlayer!! - player?.setCurrentPlayer(castPlayer!!) - //o seek to foi mais rapido, testar - player?.wrappedPlayer?.setMediaItems(items,index,currentPosition) - player?.wrappedPlayer?.prepare() - player?.wrappedPlayer?.play() + player?.setCurrentPlayer(castPlayer!!,castContext?.sessionManager?.currentCastSession?.remoteMediaClient) + if (items != null) { + smPlayer?.setMediaItems(items, index, currentPosition) + smPlayer?.prepare() + smPlayer?.play() + } } cast?.setOnSessionEndedCallback { - val currentPosition = player?.wrappedPlayer?.currentPosition!! - val index = player?.wrappedPlayer?.currentMediaItemIndex!! - mediaSession.player = exoPlayer!! - player?.setCurrentPlayer(exoPlayer!!) - player?.wrappedPlayer?.prepare() - player?.wrappedPlayer?.seekTo(index,currentPosition) + val currentPosition = smPlayer?.currentPosition ?: 0L + val index = smPlayer?.currentMediaItemIndex ?: 0 + exoPlayer?.let { + mediaSession.player = it + player?.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?.wrappedPlayer?.stop() + smPlayer?.stop() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -239,9 +266,9 @@ class MediaService : MediaSessionService() { ): MediaSession = mediaSession override fun onTaskRemoved(rootIntent: Intent?) { - player?.wrappedPlayer?.clearMediaItems() + smPlayer?.clearMediaItems() Log.d(TAG, "onTaskRemoved") - player?.wrappedPlayer?.stop() + smPlayer?.stop() stopSelf() super.onTaskRemoved(rootIntent) } @@ -261,7 +288,7 @@ class MediaService : MediaSessionService() { } private fun releasePossibleLeaks() { - player?.wrappedPlayer?.release() + smPlayer?.release() mediaSession.release() mediaController = null } @@ -276,11 +303,24 @@ class MediaService : MediaSessionService() { return false } + class CustomMediaItemConverter : MediaItemConverter { + override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem { + val queueItem = DefaultMediaItemConverter().toMediaQueueItem(mediaItem) + queueItem.Writer().setCustomData(JSONObject().put("credentials", cookie)) + return queueItem + } + + override fun toMediaItem(mediaQueueItem: MediaQueueItem): MediaItem { + return DefaultMediaItemConverter().toMediaItem(mediaQueueItem) + } + + } + fun updateMediaUri(index: Int, uri: String?) { // if (index != player?.currentMediaItemIndex) { - val media = player?.wrappedPlayer?.getMediaItemAt(index) + val media = smPlayer?.getMediaItemAt(index) media?.associatedMedia?.let { - player?.wrappedPlayer?.removeMediaItem(index) + smPlayer?.removeMediaItem(index) player?.addMediaSource( index, prepare( cookie, @@ -294,8 +334,15 @@ class MediaService : MediaSessionService() { } fun toggleShuffle(positionsList: List>) { - player?.wrappedPlayer?.shuffleModeEnabled = !(player?.shuffleModeEnabled ?: false) - player?.wrappedPlayer?.shuffleModeEnabled?.let { + //TODO: AJUSTAR COM CAST + if(smPlayer is CastPlayer){ + player?.remoteMediaClient?.queueShuffle(JSONObject()) + val queue = player?.remoteMediaClient?.mediaQueue + val items = queue?.getItemAtIndex(0) + return + } + smPlayer?.shuffleModeEnabled = !(smPlayer?.shuffleModeEnabled ?: false) + smPlayer?.shuffleModeEnabled?.let { if (it) { PlayerSingleton.shuffledIndices.clear() for (e in positionsList) { @@ -307,9 +354,9 @@ class MediaService : MediaSessionService() { ) Log.d( TAG, - "toggleShuffle - shuffleOrder is null: ${shuffleOrder == null} | shuffledIndices: ${PlayerSingleton.shuffledIndices.size} - ${player?.mediaItemCount}" + "toggleShuffle - shuffledIndices: ${PlayerSingleton.shuffledIndices.size} - ${player?.mediaItemCount}" ) - player!!.setShuffleOrder(shuffleOrder!!) + player?.setShuffleOrder(shuffleOrder!!) } playerChangeNotifier?.onShuffleModeEnabled(it) } @@ -342,8 +389,8 @@ class MediaService : MediaSessionService() { ) this.autoPlay = autoPlay PlayerSingleton.shouldNotifyTransition = shouldNotifyTransition - if (player?.wrappedPlayer?.mediaItemCount == 0) { - player?.wrappedPlayer?.playWhenReady = autoPlay + if (smPlayer?.mediaItemCount == 0) { + smPlayer?.playWhenReady = autoPlay } // enqueueLoadOnly = autoPlay android.util.Log.d( @@ -360,12 +407,7 @@ class MediaService : MediaSessionService() { mediaSources.add(prepare(cookie, medias[i], "")) } player?.addMediaSources(mediaSources) - player?.wrappedPlayer?.prepare() -// PlayerSingleton.playerChangeNotifier?.notifyItemTransition("createMediaSource") -// playerChangeNotifier?.currentMediaIndex( -// currentIndex(), -// "createMediaSource", -// ) + smPlayer?.prepare() } if (PlayerSingleton.shouldNotifyTransition) { playerChangeNotifier?.notifyItemTransition("Enqueue - createMediaSource") @@ -387,6 +429,7 @@ class MediaService : MediaSessionService() { Uri.parse(urlToPrepare) } val mediaItem = MediaItem.Builder() + .setMediaId(media.id.toString()) .setUri(uri) .setMediaMetadata(metadata) .setMediaId(media.id.toString()) @@ -423,7 +466,21 @@ class MediaService : MediaSessionService() { newIndex: Int, positionsList: List> ) { - if (player?.shuffleModeEnabled == true) { + //TODO: Criei issue no media3 pq esta crashando no reorder do cast + if(smPlayer is CastPlayer){ + val mediaItems = player?.remoteMediaClient?.mediaQueue + val mediaIds = mediaItems?.itemIds + val reorderedIds = mediaIds?.toMutableList() + if (reorderedIds != null) { + val item = reorderedIds.removeAt(oldIndex) + reorderedIds.add(newIndex, item) + } + + player?.remoteMediaClient?.queueReorderItems(reorderedIds?.toIntArray()!!, newIndex, JSONObject()) + return + } + + if (smPlayer?.shuffleModeEnabled == true) { val list = PlayerSingleton.shuffledIndices.ifEmpty { positionsList.map { it["originalPosition"] ?: 0 }.toMutableList() } @@ -432,7 +489,7 @@ class MediaService : MediaSessionService() { DefaultShuffleOrder(list.toIntArray(), System.currentTimeMillis()) player?.setShuffleOrder(shuffleOrder!!) } else { - player?.wrappedPlayer?.moveMediaItem(oldIndex, newIndex) + smPlayer?.moveMediaItem(oldIndex, newIndex) } } @@ -444,7 +501,7 @@ class MediaService : MediaSessionService() { if (PlayerSingleton.shuffledIndices.isNotEmpty()) { PlayerSingleton.shuffledIndices.removeAt( PlayerSingleton.shuffledIndices.indexOf( - player?.wrappedPlayer?.currentMediaItemIndex ?: 0 + smPlayer?.currentMediaItemIndex ?: 0 ) ) } @@ -460,11 +517,11 @@ class MediaService : MediaSessionService() { } fun disableRepeatMode() { - player?.wrappedPlayer?.repeatMode = REPEAT_MODE_OFF + smPlayer?.repeatMode = REPEAT_MODE_OFF } fun repeatMode() { - player?.wrappedPlayer?.let { + smPlayer?.let { when (it.repeatMode) { REPEAT_MODE_OFF -> { it.repeatMode = REPEAT_MODE_ALL @@ -501,17 +558,17 @@ class MediaService : MediaSessionService() { return metadata } - fun play(shouldPrepare: Boolean = false) { + fun play() { // PlayerSingleton.performAndEnableTracking { - if (shouldPrepare) { - player?.wrappedPlayer?.prepare() - } - player?.wrappedPlayer?.play() + if (smPlayer?.playbackState == STATE_IDLE ) { + smPlayer?.prepare() + } + smPlayer?.play() // } } fun setRepeatMode(mode: String) { - player?.wrappedPlayer?.repeatMode = when (mode) { + smPlayer?.repeatMode = when (mode) { "off" -> REPEAT_MODE_OFF "one" -> REPEAT_MODE_ONE "all" -> REPEAT_MODE_ALL @@ -520,43 +577,43 @@ class MediaService : MediaSessionService() { } fun playFromQueue(position: Int, timePosition: Long, loadOnly: Boolean = false) { - player?.wrappedPlayer?.playWhenReady = !loadOnly + smPlayer?.playWhenReady = !loadOnly if (loadOnly) { seekToLoadOnly = true } - player?.wrappedPlayer?.seekTo( - if (player?.wrappedPlayer?.shuffleModeEnabled == true) PlayerSingleton.shuffledIndices[position] else position, + smPlayer?.seekTo( + if (smPlayer?.shuffleModeEnabled == true) PlayerSingleton.shuffledIndices[position] else position, timePosition, ) if (!loadOnly) { - player?.wrappedPlayer?.prepare() + smPlayer?.prepare() playerChangeNotifier?.notifyItemTransition("playFromQueue") } } fun removeAll() { - player?.wrappedPlayer?.stop() - player?.wrappedPlayer?.clearMediaItems() + smPlayer?.stop() + smPlayer?.clearMediaItems() } fun seek(position: Long, playWhenReady: Boolean) { - player?.wrappedPlayer?.seekTo(position) - player?.wrappedPlayer?.playWhenReady = playWhenReady + smPlayer?.seekTo(position) + smPlayer?.playWhenReady = playWhenReady } fun pause() { - player?.wrappedPlayer?.pause() + smPlayer?.pause() } fun stop() { - player?.wrappedPlayer?.stop() + smPlayer?.stop() } fun togglePlayPause() { - if (player?.wrappedPlayer?.isPlaying == true) { + if (smPlayer?.isPlaying == true) { pause() } else { play() @@ -564,7 +621,6 @@ class MediaService : MediaSessionService() { } private fun releaseAndPerformAndDisableTracking() { - player?.wrappedPlayer?.stop() + 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 794af4ff..bf9417c4 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 @@ -82,10 +82,8 @@ class MediaSessionConnection( sendCommand(PLAY_FROM_QUEUE_METHOD, bundle) } - fun play(shouldPrepare: Boolean = false) { - val bundle = Bundle() - bundle.putBoolean("shouldPrepare", shouldPrepare) - sendCommand("play", bundle) + fun play() { + sendCommand("play", null) } fun cast(id: String){ 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 b77c58a7..17870897 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 @@ -8,7 +8,7 @@ import androidx.media3.common.Player.* class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { fun notifyStateChange(state: @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 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 index 987c11f5..ce97c5e7 100644 --- 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 @@ -2,7 +2,6 @@ package br.com.suamusica.player import androidx.media3.common.MediaItem import androidx.media3.common.Player -import com.google.android.gms.cast.MediaQueueItem import java.util.WeakHashMap val mediaItemMediaAssociations = WeakHashMap() @@ -20,14 +19,4 @@ var MediaItem.associatedMedia: Media? get() = mediaItemMediaAssociations[this] set(value) { mediaItemMediaAssociations[this] = value - } - -fun Player.buildMediaQueueItems(convert: (MediaItem) -> MediaQueueItem): List { - val items = mutableListOf() - for (i in 0 until mediaItemCount) { - getMediaItemAt(i).let { mediaItem -> - items.add(convert(mediaItem)) - } - } - return items -} \ No newline at end of file + } \ No newline at end of file diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt index 3dccdf47..27a92c61 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 @@ -51,7 +51,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" @@ -91,9 +90,9 @@ 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 + // (binding.activity.applicationInfo.flags and ApplicationInfo.FLAG_STOPPED) == ApplicationInfo.FLAG_STOPPED // if (!isStopped) { - alreadyAttachedToActivity = true + alreadyAttachedToActivity = true // } } @@ -123,11 +122,8 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { if (call.method == ENQUEUE) { val batch: Map = call.arguments()!! cookie = if (batch.containsKey("cookie")) batch["cookie"] as String else cookie - PlayerSingleton.externalPlayback = - if (batch.containsKey("externalplayback")) batch["externalplayback"].toString() == "true" else PlayerSingleton.externalPlayback } else { cookie = call.argument("cookie") ?: cookie - PlayerSingleton.externalPlayback = call.argument("externalplayback") } Log.d( TAG, @@ -153,8 +149,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { PlayerSingleton.mediaSessionConnection?.cast(id) } PLAY_METHOD -> { - val shouldPrepare = call.argument("shouldPrepare") ?: false - PlayerSingleton.mediaSessionConnection?.play(shouldPrepare) + PlayerSingleton.mediaSessionConnection?.play() } SET_REPEAT_MODE -> { @@ -209,17 +204,11 @@ 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 - ) } } 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 c6276fcc..10371daa 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,7 +7,6 @@ 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 @@ -26,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() { 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 index b4d2d585..86899e14 100644 --- 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 @@ -21,46 +21,36 @@ 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, - private var context: Context ) : ForwardingPlayer(currentPlayer) { private var playerEventListener: Player.Listener? = null private val TAG = "PlayerSwitcher" private var progressTracker: ProgressTracker? = null + var remoteMediaClient: RemoteMediaClient? = null init { playerEventListener?.let { currentPlayer.removeListener(it) } setupPlayerListener() } - fun setCurrentPlayer(newPlayer: Player) { + fun setCurrentPlayer(newPlayer: Player, remoteMediaClient: RemoteMediaClient? = null) { if (this.currentPlayer === newPlayer) { return } - - // Salva o estado atual do player - val playerState = capturePlayerState() - - // Remove listener do player anterior + this.remoteMediaClient = remoteMediaClient + val playerState = savePlayerState() playerEventListener?.let { currentPlayer.removeListener(it) } - - // Para e limpa o player anterior stopAndClearCurrentPlayer() - - // Atualiza para o novo player this.currentPlayer = newPlayer - - // Restaura o estado no novo player - if(currentPlayer is CastPlayer) { + if (currentPlayer is CastPlayer) { restorePlayerState(playerState) } - - // Adiciona listener no novo player setupPlayerListener() } @@ -68,10 +58,10 @@ class PlayerSwitcher( val playbackPositionMs: Long = C.TIME_UNSET, val currentItemIndex: Int = C.INDEX_UNSET, val playWhenReady: Boolean = false, - val mediaItems: List = emptyList() + val mediaItems: List = emptyList(), ) - private fun capturePlayerState(): PlayerState { + private fun savePlayerState(): PlayerState { return PlayerState( playbackPositionMs = if (currentPlayer.playbackState != STATE_ENDED) currentPlayer.currentPosition else C.TIME_UNSET, currentItemIndex = currentPlayer.currentMediaItemIndex, @@ -110,13 +100,15 @@ class PlayerSwitcher( } override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - playerChangeNotifier?.notifyPlaying(isPlaying) - if (isPlaying) { - startTrackingProgress() - } else { - stopTrackingProgress() + super.onIsPlayingChanged(isPlaying) + if(lastState != STATE_BUFFERING) { + playerChangeNotifier?.notifyPlaying(isPlaying) } + if (isPlaying) { + startTrackingProgress() + } else { + stopTrackingProgress() + } } override fun onMediaItemTransition( @@ -161,7 +153,7 @@ class PlayerSwitcher( } override fun onPlayerError(error: PlaybackException) { - android.util.Log.d( + Log.d( "#NATIVE LOGS ==>", "onPlayerError cause ${error.cause.toString()}" ) @@ -192,12 +184,12 @@ class PlayerSwitcher( } fun currentIndex(): Int { - val position = if (currentPlayer.shuffleModeEnabled == true) { + val position = if (currentPlayer.shuffleModeEnabled) { PlayerSingleton.shuffledIndices.indexOf( - currentPlayer.currentMediaItemIndex ?: 0 + currentPlayer.currentMediaItemIndex ) } else { - currentPlayer.currentMediaItemIndex ?: 0 + currentPlayer.currentMediaItemIndex } return position } @@ -207,6 +199,18 @@ class PlayerSwitcher( progressTracker = null } + fun customShuffleModeEnabled(shuffleModeEnabled: Boolean) { + when (currentPlayer) { + is ExoPlayer -> { + (currentPlayer as ExoPlayer).shuffleModeEnabled = shuffleModeEnabled + } + + is CastPlayer -> { + currentPlayer.shuffleModeEnabled = shuffleModeEnabled + } + } + } + fun setShuffleOrder(shuffleOrder: ShuffleOrder) { if (currentPlayer is ExoPlayer) { (currentPlayer as ExoPlayer).setShuffleOrder(shuffleOrder) diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index ff5bbd11..5b58df7b 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -41,7 +41,7 @@ var media1 = Media( ); var media3 = Media( - id: 1, + id: 3, albumTitle: "Album unsigned", albumId: 1, name: "Track unsigned", @@ -59,7 +59,7 @@ var media3 = Media( ); var media4 = Media( - id: 1, + id: 4, albumTitle: "É o Grelo", albumId: 1, name: "01 - VIDA LOK4 part.1", diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 5c3edd52..0b933afb 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -270,13 +270,8 @@ class Player { int get currentIndex => _queue.index; - Future play({bool shouldPrepare = false}) async { - await _invokeMethod( - 'play', - { - 'shouldPrepare': shouldPrepare, - }, - ); + Future play() async { + await _invokeMethod('play'); return Ok; } @@ -384,16 +379,11 @@ class Player { } } - Future updateNotification({ + Future updateFavorite({ required bool isFavorite, required int id, }) async { - // final index = _queue.items.indexWhere((item) => item.id == id); - // if (index != -1) { - // _queue.items[index] = - // _queue.items[index].copyWith(isFavorite: isFavorite); - // } - return _channel.invokeMethod('update_notification', { + return _channel.invokeMethod('update_favorite', { 'isFavorite': isFavorite, 'idFavorite': id, }).then((result) => result); @@ -407,16 +397,15 @@ class Player { void addUsingPlayer(Event event) => _addUsingPlayer(player, event); Future stop() async { - // _notifyPlayerStatusChangeEvent(EventType.STOP_REQUESTED); - // final int result = await _invokeMethod('stop'); + _notifyPlayerStatusChangeEvent(EventType.STOP_REQUESTED); + final int result = await _invokeMethod('stop'); - // if (result == Ok) { - // state = PlayerState.STOPPED; - // _notifyPlayerStatusChangeEvent(EventType.STOPPED); - // } + if (result == Ok) { + state = PlayerState.STOPPED; + _notifyPlayerStatusChangeEvent(EventType.STOPPED); + } - // return result; - return Ok; + return result; } Future release() async { @@ -507,6 +496,12 @@ class Player { player.state = PlayerState.values[state]; switch (player.state) { case PlayerState.STATE_READY: + _notifyPlayerStateChangeEvent( + player, + EventType.STATE_READY, + error, + ); + break; case PlayerState.IDLE: _notifyPlayerStateChangeEvent( player, diff --git a/packages/player/lib/src/player_state.dart b/packages/player/lib/src/player_state.dart index c3d51628..68750733 100644 --- a/packages/player/lib/src/player_state.dart +++ b/packages/player/lib/src/player_state.dart @@ -9,6 +9,6 @@ enum PlayerState { SEEK_END, BUFFER_EMPTY, ITEM_TRANSITION, - STATE_ENDED, STATE_READY, + STATE_ENDED, } From a0cf2ce159039a6b783f87b29a85cc8a4a4eb7ef Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 15 Jan 2025 10:05:15 -0300 Subject: [PATCH 08/34] added finished event --- packages/player/lib/src/player.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 0b933afb..42e46c0b 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -456,6 +456,11 @@ class Player { } } + static Future _handleOnComplete(Player player) async { + player.state = PlayerState.COMPLETED; + _notifyPlayerStateChangeEvent(player, EventType.FINISHED_PLAYING, ""); + } + static Future _doHandlePlatformCall(MethodCall call) async { final currentMedia = _queue.current; final currentIndex = _queue.index; @@ -563,7 +568,7 @@ class Player { break; case PlayerState.COMPLETED: - // _handleOnComplete(player); + _handleOnComplete(player); break; case PlayerState.STATE_ENDED: From e3f2193f17019baf91314a3f254a40b8e0c3a05a Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 15 Jan 2025 10:24:39 -0300 Subject: [PATCH 09/34] improve logs --- .../kotlin/br/com/suamusica/player/Cast.kt | 36 +++++++++---------- .../br/com/suamusica/player/MediaService.kt | 6 ++-- .../suamusica/player/PlayerChangeNotifier.kt | 8 ++--- 3 files changed, 25 insertions(+), 25 deletions(-) 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 index 458c8943..ed885041 100644 --- 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 @@ -55,17 +55,17 @@ class CastManager( 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()) + 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()) + 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()) + Log.d(TAG, "#NATIVE LOGS CAST ==> Route changed: " + route.getName()) } override fun onRouteSelected( @@ -75,7 +75,7 @@ class CastManager( ) { Log.d( TAG, - "#NATIVE LOGS ==> CAST: Route selected: " + route.getName() + ", reason: " + reason + "#NATIVE LOGS CAST ==> Route selected: " + route.getName() + ", reason: " + reason ) } @@ -86,7 +86,7 @@ class CastManager( ) { Log.d( TAG, - "#NATIVE LOGS ==> CAST: Route unselected: " + route.getName() + ", reason: " + reason + "#NATIVE LOGS CAST ==> Route unselected: " + route.getName() + ", reason: " + reason ) } } @@ -243,7 +243,7 @@ class CastManager( override fun onCastStateChanged(state: Int) { Log.d( TAG, - "#NATIVE LOGS ==> CAST: RECEIVER UPDATE AVAILABLE ${CastState.toString(state)}" + "#NATIVE LOGS CAST ==> RECEIVER UPDATE AVAILABLE ${CastState.toString(state)}" ) if (alreadyConnected && state == CastState.NOT_CONNECTED) { @@ -261,56 +261,56 @@ class CastManager( //SessionAvailabilityListener override fun onCastSessionAvailable() { - Log.d(TAG, "#NATIVE LOGS ==> CAST - SessionAvailabilityListener: onCastSessionAvailable") + Log.d(TAG, "#NATIVE LOGS CAST ==>- SessionAvailabilityListener: onCastSessionAvailable") } override fun onCastSessionUnavailable() { - Log.d(TAG, "#NATIVE LOGS ==> CAST - SessionAvailabilityListener: onCastSessionUnavailable") + Log.d(TAG, "#NATIVE LOGS CAST ==>- SessionAvailabilityListener: onCastSessionUnavailable") } //PendingResult.StatusListener override fun onComplete(status: Status) { - Log.d(TAG, "#NATIVE LOGS ==> CAST: onComplete $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") + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionEnded") } override fun onSessionEnding(p0: Session) { - Log.d(TAG, "#NATIVE LOGS ==> CAST: onSessionEnding") + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionEnding") } override fun onSessionResumeFailed(p0: Session, p1: Int) { - Log.d(TAG, "#NATIVE LOGS ==> CAST: onSessionResumeFailed") + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResumeFailed") } override fun onSessionResumed(p0: Session, p1: Boolean) { - Log.d(TAG, "#NATIVE LOGS ==> CAST: onSessionResumed") + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResumed") } override fun onSessionResuming(p0: Session, p1: String) { - Log.d(TAG, "#NATIVE LOGS ==> CAST: onSessionResuming") + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResuming") } override fun onSessionStartFailed(p0: Session, p1: Int) { - Log.d(TAG, "#NATIVE LOGS ==> CAST: onSessionStartFailed $p0, $p1") + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionStartFailed $p0, $p1") } override fun onSessionStarted(p0: Session, p1: String) { - Log.d(TAG, "#NATIVE LOGS ==> CAST: onCastSessionUnavailable") + Log.d(TAG, "#NATIVE LOGS CAST ==> onCastSessionUnavailable") onSessionEndedCallback?.invoke() } override fun onSessionStarting(p0: Session) { - Log.d(TAG, "#NATIVE LOGS ==> CAST: $p0 onSessionStarting") + 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") + Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionSuspended") } // var MediaItem.associatedMedia: Media? 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 e1f12607..39414acd 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 @@ -393,9 +393,9 @@ class MediaService : MediaSessionService(){ 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: ${player?.mediaItemCount} | shouldNotifyTransition: $shouldNotifyTransition" ) addToQueue(medias) } 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 17870897..def8da7f 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt @@ -14,7 +14,7 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { STATE_READY -> PlayerState.STATE_READY else -> PlayerState.IDLE } - Log.i("Player", "#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 +23,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) } @@ -41,11 +41,11 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { 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) { From ee758a7dc5bba994baa7ef1677fed523264336c6 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 15 Jan 2025 14:49:59 -0300 Subject: [PATCH 10/34] fix seek behavior when rewind --- .../player/MediaButtonEventHandler.kt | 35 +++++++++++-------- .../br/com/suamusica/player/MediaService.kt | 12 ++++--- .../player/MediaSessionConnection.kt | 10 +++--- .../suamusica/player/PlayerChangeNotifier.kt | 16 ++++----- .../br/com/suamusica/player/PlayerPlugin.kt | 20 +++++------ .../br/com/suamusica/player/PlayerSwitcher.kt | 5 ++- packages/player/lib/src/player.dart | 15 ++++++-- 7 files changed, 68 insertions(+), 45 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt index 6ce2bbbd..0720cfaf 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt @@ -27,7 +27,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.ID_FAVORITE_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.ID_URI_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.INDEXES_TO_REMOVE import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT -import br.com.suamusica.player.PlayerPlugin.Companion.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,6 +36,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 @@ -70,7 +71,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)) @@ -125,26 +126,30 @@ class MediaButtonEventHandler( buildIcons() } - if(customCommand.customAction == "cast"){ + if (customCommand.customAction == "cast") { // mediaService.cast(args.getString("cast_id")) mediaService.castWithCastPlayer(args.getString("cast_id")) } - if(customCommand.customAction == UPDATE_IS_PLAYING){ + if (customCommand.customAction == UPDATE_IS_PLAYING) { buildIcons() } - if (customCommand.customAction == "seek") { - mediaService.seek(args.getLong("position"), args.getBoolean("playWhenReady")) + if (customCommand.customAction == SEEK_METHOD) { + mediaService.seek( + args.getLong("position"), + args.getBoolean("playWhenReady"), + args.getBoolean("shouldNotifyTransition") + ) } 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() } @@ -191,20 +196,20 @@ class MediaButtonEventHandler( } if (customCommand.customAction == SET_REPEAT_MODE) { val mode = args.getString("mode") - mediaService.setRepeatMode(mode ?:"") + mediaService.setRepeatMode(mode ?: "") } if (customCommand.customAction == PLAY_FROM_QUEUE_METHOD) { mediaService.playFromQueue( args.getInt(POSITION_ARGUMENT), args.getLong(TIME_POSITION_ARGUMENT), args.getBoolean( - LOAD_ONLY + LOAD_ONLY_ARGUMENT ), ) } if (customCommand.customAction == "notification_previous" || customCommand.customAction == "previous") { - if(session.player.hasPreviousMediaItem()){ - session.player.seekToPreviousMediaItem() - }else{ + if (session.player.hasPreviousMediaItem()) { + session.player.seekToPreviousMediaItem() + } else { session.player.seekToPrevious() } PlayerSingleton.shouldNotifyTransition = true @@ -376,9 +381,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 39414acd..1f4bc900 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 @@ -43,8 +43,9 @@ import androidx.media3.session.MediaController import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService -import br.com.suamusica.player.PlayerPlugin.Companion.FALLBACK_URL +import br.com.suamusica.player.PlayerPlugin.Companion.FALLBACK_URL_ARGUMENT import br.com.suamusica.player.PlayerPlugin.Companion.IS_FAVORITE_ARGUMENT +import br.com.suamusica.player.PlayerPlugin.Companion.SEEK_METHOD import br.com.suamusica.player.PlayerPlugin.Companion.cookie import br.com.suamusica.player.PlayerSingleton.playerChangeNotifier import br.com.suamusica.player.media.parser.SMHlsPlaylistParserFactory @@ -325,7 +326,7 @@ class MediaService : MediaSessionService(){ index, prepare( cookie, it, - uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL) ?: "" + uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL_ARGUMENT) ?: "" ) ) // player?.prepare() @@ -543,7 +544,7 @@ 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) @@ -599,9 +600,12 @@ class MediaService : MediaSessionService(){ } - fun seek(position: Long, playWhenReady: Boolean) { + fun seek(position: Long, playWhenReady: Boolean, shouldNotifyTransition:Boolean) { smPlayer?.seekTo(position) smPlayer?.playWhenReady = playWhenReady + if(shouldNotifyTransition){ + playerChangeNotifier?.notifyItemTransition(SEEK_METHOD) + } } fun pause() { 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 bf9417c4..1beb9561 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 @@ -16,7 +16,7 @@ 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 @@ -78,7 +79,7 @@ class MediaSessionConnection( 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) } @@ -183,11 +184,12 @@ class MediaSessionConnection( sendCommand("stop", null) } - fun seek(position: Long, playWhenReady: Boolean) { + fun seek(position: Long, playWhenReady: Boolean, shouldNotifyTransition:Boolean) { val bundle = Bundle() bundle.putLong("position", position) bundle.putBoolean("playWhenReady", playWhenReady) - sendCommand("seek", bundle) + bundle.putBoolean("shouldNotifyTransition", shouldNotifyTransition) + sendCommand(SEEK_METHOD, bundle) } fun release() { 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 def8da7f..ec674a56 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 @@ -32,14 +32,14 @@ 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 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 Notify ==> notifyItemTransition | FROM: $from") channelManager.notifyItemTransition("sua-musica-player") 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 27a92c61..75a4d4c9 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 @@ -15,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" @@ -31,9 +29,9 @@ 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 @@ -215,7 +213,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { PLAY_FROM_QUEUE_METHOD -> { val position = call.argument(POSITION_ARGUMENT) ?: 0 val timePosition = call.argument(TIME_POSITION_ARGUMENT) ?: 0 - val loadOnly = call.argument(LOAD_ONLY) ?: false + val loadOnly = call.argument(LOAD_ONLY_ARGUMENT) ?: false PlayerSingleton.mediaSessionConnection?.playFromQueue( position, timePosition.toLong(), @@ -245,7 +243,9 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { SEEK_METHOD -> { val position = call.argument(POSITION_ARGUMENT)!! - PlayerSingleton.mediaSessionConnection?.seek(position, true) + val shouldNotifyTransition = call.argument(SHOULD_NOTIFY_TRANSITION_ARGUMENT)!! + val playWhenReady = call.argument(PLAY_WHEN_READY_ARGUMENT)!! + PlayerSingleton.mediaSessionConnection?.seek(position, playWhenReady, shouldNotifyTransition) } REMOVE_NOTIFICATION_METHOD -> { 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 index 86899e14..1973e3ca 100644 --- 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 @@ -124,10 +124,13 @@ class PlayerSwitcher( ) } mediaButtonEventHandler.buildIcons() + + //TODO: verificar o motivo, tinha um, mas não lembro if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED || !PlayerSingleton.shouldNotifyTransition) { return } - playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: ${reason} | shouldNotifyTransition: ${PlayerSingleton.shouldNotifyTransition}") + + playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: $reason | shouldNotifyTransition: ${PlayerSingleton.shouldNotifyTransition}") PlayerSingleton.shouldNotifyTransition = false } diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 42e46c0b..5b54e702 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -346,7 +346,8 @@ class Player { Future previous({bool isFromChromecast = false}) async { if (_queue.shouldRewind()) { - seek(Duration(milliseconds: 0)); + //If is rewind, we need to notify the transition, following the current behavior + seek(Duration(milliseconds: 0), shouldNotifyTransition: true); print("#APP LOGS ==> shouldRewind"); return Ok; } @@ -431,9 +432,17 @@ class Player { .invokeMethod('toggle_shuffle', {'positionsList': getPositionsList()}); } - Future seek(Duration position) { + Future seek( + Duration position, { + bool playWhenReady = true, + bool shouldNotifyTransition = false, + }) { _notifyPlayerStateChangeEvent(this, EventType.SEEK_START, ""); - return _invokeMethod('seek', {'position': position.inMilliseconds}); + return _invokeMethod('seek', { + 'position': position.inMilliseconds, + 'playWhenReady': playWhenReady, + 'shouldNotifyTransition': shouldNotifyTransition, + }); } Future setVolume(double volume) { From 701fb5bdde079a506179b80a33d440a91472c601 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 16 Jan 2025 13:53:42 -0300 Subject: [PATCH 11/34] set shouldNotifyTransition false to default --- .../main/kotlin/br/com/suamusica/player/MediaService.kt | 8 ++------ .../br/com/suamusica/player/PlayerChangeNotifier.kt | 1 - .../main/kotlin/br/com/suamusica/player/PlayerSwitcher.kt | 1 - packages/player/lib/src/player.dart | 2 +- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt index 1f4bc900..8f9c4109 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 @@ -384,10 +384,6 @@ class MediaService : MediaSessionService(){ autoPlay: Boolean, shouldNotifyTransition: Boolean, ) { - Log.d( - TAG, - "enqueue: mediaItemCount: ${player?.mediaItemCount} | autoPlay: $autoPlay" - ) this.autoPlay = autoPlay PlayerSingleton.shouldNotifyTransition = shouldNotifyTransition if (smPlayer?.mediaItemCount == 0) { @@ -410,8 +406,8 @@ class MediaService : MediaSessionService(){ player?.addMediaSources(mediaSources) smPlayer?.prepare() } - if (PlayerSingleton.shouldNotifyTransition) { - playerChangeNotifier?.notifyItemTransition("Enqueue - createMediaSource") + if (autoPlay) { + playerChangeNotifier?.notifyItemTransition("#NATIVE LOGS MEDIA SERVICE ==> Enqueue - createMediaSource") } } 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 ec674a56..6b255b87 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 @@ -49,7 +49,6 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { 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) { 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 index 1973e3ca..6baf25b4 100644 --- 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 @@ -125,7 +125,6 @@ class PlayerSwitcher( } mediaButtonEventHandler.buildIcons() - //TODO: verificar o motivo, tinha um, mas não lembro if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED || !PlayerSingleton.shouldNotifyTransition) { return } diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 5b54e702..48466f2d 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -141,7 +141,7 @@ class Player { bool autoPlay = false, bool saveOnTop = false, bool alreadyAddedToStorage = false, - bool shouldNotifyTransition = true, + bool shouldNotifyTransition = false, }) async { if (!alreadyAddedToStorage) { _queue.addAll(items, saveOnTop: saveOnTop); From 2f521d10067216ed76004826f233b46ccb3d9837 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Fri, 17 Jan 2025 14:07:30 -0300 Subject: [PATCH 12/34] fix exemple and use events from player --- .../player/MediaButtonEventHandler.kt | 12 +- .../br/com/suamusica/player/MediaService.kt | 36 +-- .../player/MediaSessionConnection.kt | 7 +- .../br/com/suamusica/player/PlayerPlugin.kt | 9 +- .../br/com/suamusica/player/PlayerSwitcher.kt | 19 +- packages/player/example/lib/sm_player.dart | 278 ++++++++++-------- packages/player/lib/src/player.dart | 7 +- 7 files changed, 195 insertions(+), 173 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt index 0720cfaf..edbfeb6c 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 @@ -21,7 +21,7 @@ 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 @@ -81,7 +81,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)) @@ -139,7 +139,6 @@ class MediaButtonEventHandler( mediaService.seek( args.getLong("position"), args.getBoolean("playWhenReady"), - args.getBoolean("shouldNotifyTransition") ) } if (customCommand.customAction == FAVORITE) { @@ -212,10 +211,8 @@ class MediaButtonEventHandler( } else { session.player.seekToPrevious() } - PlayerSingleton.shouldNotifyTransition = true } if (customCommand.customAction == "notification_next" || customCommand.customAction == "next") { - PlayerSingleton.shouldNotifyTransition = true session.player.seekToNextMediaItem() } if (customCommand.customAction == "pause") { @@ -252,14 +249,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 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 8f9c4109..331a3eae 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 @@ -5,14 +5,9 @@ import android.app.ActivityManager import android.app.PendingIntent import android.app.Service import android.content.Intent -import android.media.AudioFocusRequest -import android.media.AudioManager -import android.media.AudioManager.OnAudioFocusChangeListener import android.net.Uri import android.os.Build import android.os.Bundle -import androidx.media.AudioFocusRequestCompat -import androidx.media.AudioManagerCompat import androidx.media3.cast.CastPlayer import androidx.media3.cast.DefaultMediaItemConverter import androidx.media3.cast.MediaItemConverter @@ -64,7 +59,6 @@ import java.io.File import java.util.Collections -const val NOW_PLAYING_CHANNEL: String = "br.com.suamusica.media.NOW_PLAYING" const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 @UnstableApi @@ -89,12 +83,8 @@ class MediaService : MediaSessionService(){ 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 autoPlay: Boolean = true - private val channel = Channel>(Channel.BUFFERED) private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) @@ -294,16 +284,6 @@ class MediaService : MediaSessionService(){ 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 - } - } - return false - } - class CustomMediaItemConverter : MediaItemConverter { override fun toMediaQueueItem(mediaItem: MediaItem): MediaQueueItem { val queueItem = DefaultMediaItemConverter().toMediaQueueItem(mediaItem) @@ -318,7 +298,6 @@ class MediaService : MediaSessionService(){ } fun updateMediaUri(index: Int, uri: String?) { -// if (index != player?.currentMediaItemIndex) { val media = smPlayer?.getMediaItemAt(index) media?.associatedMedia?.let { smPlayer?.removeMediaItem(index) @@ -329,9 +308,7 @@ class MediaService : MediaSessionService(){ uri ?: media.mediaMetadata.extras?.getString(FALLBACK_URL_ARGUMENT) ?: "" ) ) -// player?.prepare() } -// } } fun toggleShuffle(positionsList: List>) { @@ -389,10 +366,9 @@ class MediaService : MediaSessionService(){ if (smPlayer?.mediaItemCount == 0) { smPlayer?.playWhenReady = autoPlay } -// enqueueLoadOnly = autoPlay Log.d( TAG, - "#NATIVE LOGS MEDIA SERVICE ==> enqueue $autoPlay | mediaItemCount: ${player?.mediaItemCount} | shouldNotifyTransition: $shouldNotifyTransition" + "#NATIVE LOGS MEDIA SERVICE ==> enqueue $autoPlay | mediaItemCount: ${player?.mediaItemCount}" ) addToQueue(medias) } @@ -584,6 +560,10 @@ class MediaService : MediaSessionService(){ if (smPlayer?.shuffleModeEnabled == true) PlayerSingleton.shuffledIndices[position] else position, timePosition, ) + + if(position > 0) { + playerChangeNotifier?.notifyPositionChange(timePosition, smPlayer?.duration ?: 0) + } if (!loadOnly) { smPlayer?.prepare() playerChangeNotifier?.notifyItemTransition("playFromQueue") @@ -593,15 +573,13 @@ class MediaService : MediaSessionService(){ fun removeAll() { smPlayer?.stop() smPlayer?.clearMediaItems() + playerChangeNotifier?.notifyStateChange(STATE_IDLE) } - fun seek(position: Long, playWhenReady: Boolean, shouldNotifyTransition:Boolean) { + fun seek(position: Long, playWhenReady: Boolean) { smPlayer?.seekTo(position) smPlayer?.playWhenReady = playWhenReady - if(shouldNotifyTransition){ - playerChangeNotifier?.notifyItemTransition(SEEK_METHOD) - } } fun pause() { 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 1beb9561..b8f591f1 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,7 +10,7 @@ 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 @@ -72,7 +72,7 @@ class MediaSessionConnection( 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) { @@ -184,11 +184,10 @@ class MediaSessionConnection( sendCommand("stop", null) } - fun seek(position: Long, playWhenReady: Boolean, shouldNotifyTransition:Boolean) { + fun seek(position: Long, playWhenReady: Boolean) { val bundle = Bundle() bundle.putLong("position", position) bundle.putBoolean("playWhenReady", playWhenReady) - bundle.putBoolean("shouldNotifyTransition", shouldNotifyTransition) sendCommand(SEEK_METHOD, bundle) } diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt index 75a4d4c9..47911055 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 @@ -37,7 +37,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { // 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" @@ -117,7 +117,7 @@ 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 } else { @@ -128,7 +128,7 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { "method: ${call.method}" ) when (call.method) { - ENQUEUE -> { + ENQUEUE_METHOD -> { val batch: Map = call.arguments() ?: emptyMap() val listMedia: List> = batch["batch"] as List> @@ -243,9 +243,8 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { SEEK_METHOD -> { val position = call.argument(POSITION_ARGUMENT)!! - val shouldNotifyTransition = call.argument(SHOULD_NOTIFY_TRANSITION_ARGUMENT)!! val playWhenReady = call.argument(PLAY_WHEN_READY_ARGUMENT)!! - PlayerSingleton.mediaSessionConnection?.seek(position, playWhenReady, shouldNotifyTransition) + PlayerSingleton.mediaSessionConnection?.seek(position, playWhenReady) } REMOVE_NOTIFICATION_METHOD -> { 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 index 6baf25b4..69ae8d21 100644 --- 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 @@ -116,21 +116,26 @@ class PlayerSwitcher( reason: @Player.MediaItemTransitionReason Int ) { super.onMediaItemTransition(mediaItem, reason) - Log.d(TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason") - if ((currentPlayer.mediaItemCount ?: 0) > 0) { + val hasItems = (currentPlayer.mediaItemCount ?: 0) > 0 + + val shouldNotify = (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && hasItems) && !PlayerSingleton.shouldNotifyTransition + Log.d(TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason | shouldNotify: $shouldNotify") + + if (shouldNotify) { + return + } + + if (hasItems) { playerChangeNotifier?.currentMediaIndex( currentIndex(), "onMediaItemTransition", ) } - mediaButtonEventHandler.buildIcons() - if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED || !PlayerSingleton.shouldNotifyTransition) { - return - } + mediaButtonEventHandler.buildIcons() playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: $reason | shouldNotifyTransition: ${PlayerSingleton.shouldNotifyTransition}") - PlayerSingleton.shouldNotifyTransition = false +// PlayerSingleton.shouldNotifyTransition = false } var lastState = PlaybackStateCompat.STATE_NONE - 1 diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 5b58df7b..5ede242b 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -101,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() { @@ -122,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(); @@ -139,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: } @@ -216,14 +222,7 @@ class _SMPlayerState extends State { void playOrPause() async { print("Player State: ${_player.state}"); - if (_player.state == PlayerState.IDLE && _player.currentMedia != null) { - int result = await _player.play(); - if (result == Player.Ok) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text('Audio is now playing!!!!'))); - } - } else if (_player.state == PlayerState.BUFFERING && - _player.currentMedia != null) { + if (_player.state == PlayerState.STATE_READY) { int result = await _player.play(); if (result == Player.Ok) { ScaffoldMessenger.of(context) @@ -241,22 +240,18 @@ class _SMPlayerState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Audio is now playing again!!!!'))); } - } else { - int? result = await _player.next(); - if (result == Player.Ok) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Audio is now playing again!!!!'))); - } } } void seek(double p) { - setState(() { - position = Duration(milliseconds: p.round()); - if (_player.state != PlayerState.STOPPED) { - _player.seek(position); - } - }); + setState( + () { + position = Duration(milliseconds: p.round()); + if (_player.state != PlayerState.STOPPED) { + _player.seek(position); + } + }, + ); } void next() { @@ -346,6 +341,82 @@ class _SMPlayerState extends State { List foundServices = []; return Scaffold( + drawer: Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: AppColors.primary, + ), + child: Text( + 'Media Controls', + style: TextStyle( + color: Colors.white, + fontSize: 24, + ), + ), + ), + ListTile( + leading: Icon(Icons.delete), + title: Text('Remove all'), + onTap: () { + _player.stop(); + _player.removeAll(); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.queue), + title: Text('Add all medias (AutoPlay)'), + onTap: () { + setState(() { + _player.enqueueAll( + [media1, media2, media3, media4], + autoPlay: true, + ); + }); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.queue), + title: Text('Add all medias'), + onTap: () { + _player.enqueueAll( + [media1, media2, media3, media4], + ); + }, + ), + ListTile( + leading: Icon(Icons.folder), + title: Text('Add local file'), + onTap: () { + pickLocalFile(); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.play_arrow), + title: Text('Play from queue (second - 50 seconds)'), + onTap: () { + _player.playFromQueue( + 1, + loadOnly: true, + position: Duration(seconds: 50), + ); + }, + ), + ListTile( + leading: Icon(Icons.add), + title: Text('Add media1'), + onTap: () { + _player.enqueueAll([media1]); + }, + ), + ], + ), + ), appBar: AppBar( title: Text('SM Player'), actions: [ @@ -368,48 +439,6 @@ class _SMPlayerState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.delete), - onPressed: () { - _player.removeAll(); - }, - tooltip: 'Remove all', - ), - IconButton( - icon: Icon(Icons.add), - onPressed: () { - _player.enqueueAll( - [media1, media2, media3, media4], - autoPlay: true, - ); - }, - tooltip: 'Add media', - ), - IconButton( - icon: Icon(Icons.queue), - onPressed: () { - _player.enqueueAll( - [media1, media2, media3, media4], - ); - }, - tooltip: 'Add media', - ), - IconButton( - icon: Icon(Icons.folder), - onPressed: pickLocalFile, - tooltip: 'Add local file', - ), - IconButton( - icon: Icon(Icons.play_arrow), - onPressed: () => _player.playFromQueue(1, - loadOnly: true, position: Duration(seconds: 50)), - tooltip: 'Play from queue', - ), - ], - ), Stack( children: [ Wrap( @@ -473,47 +502,58 @@ class _SMPlayerState extends State { )), ), 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), + 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: _player.state == PlayerState.PLAYING - ? SvgPicture.asset(UIData.btPlayerPause) - : SvgPicture.asset(UIData.btPlayerPlay), - onPressed: playOrPause, - )), - Container( - margin: EdgeInsets.only(left: 8, right: 8), - child: Material( + 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), - ))), - ), - ], - )), + onPressed: next, + iconSize: 40, + icon: Container( + child: SvgPicture.asset(UIData.btPlayerNext), + ), + ), + ), + ), + ], + ), + ), Container( margin: EdgeInsets.only(right: 8), child: Material( @@ -529,7 +569,8 @@ class _SMPlayerState extends State { ], ), SizedBox(height: 30), - Text(mediaLabel), + if (_player.items.isNotEmpty) + Text('Tocando: ${_player.items[_currentIndex].name}'), SizedBox(height: 30), Expanded( child: SizedBox( @@ -550,7 +591,14 @@ class _SMPlayerState extends State { key: Key('queueItemWidgetKey$index'), child: Container( height: 50, - color: Colors.blue[colorCodes[index % 3]], + 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}')), ), diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 48466f2d..5f3fba8b 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -177,6 +177,8 @@ class Player { 'playerId': playerId, 'shallSendEvents': _shallSendEvents, 'externalplayback': externalPlayback, + //If the batch has more than one item, we don't need to notify + //the transition, because it will be notified when the first item is played 'shouldNotifyTransition': batch.length > 1 ? false : shouldNotifyTransition, if (i == 0) ...{ @@ -346,8 +348,7 @@ class Player { Future previous({bool isFromChromecast = false}) async { if (_queue.shouldRewind()) { - //If is rewind, we need to notify the transition, following the current behavior - seek(Duration(milliseconds: 0), shouldNotifyTransition: true); + seek(Duration(milliseconds: 0)); print("#APP LOGS ==> shouldRewind"); return Ok; } @@ -435,13 +436,11 @@ class Player { Future seek( Duration position, { bool playWhenReady = true, - bool shouldNotifyTransition = false, }) { _notifyPlayerStateChangeEvent(this, EventType.SEEK_START, ""); return _invokeMethod('seek', { 'position': position.inMilliseconds, 'playWhenReady': playWhenReady, - 'shouldNotifyTransition': shouldNotifyTransition, }); } From f2f47a7a9516901e9bfba41e56e8d07c4b6e53ea Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 20 Jan 2025 11:11:18 -0300 Subject: [PATCH 13/34] ready to cast --- .../br/com/suamusica/player/MediaService.kt | 1 - .../suamusica/player/PlayerChangeNotifier.kt | 18 ++--- .../br/com/suamusica/player/PlayerSwitcher.kt | 74 ++++++++++++------- packages/player/example/lib/sm_player.dart | 2 +- packages/player/lib/src/player.dart | 2 - 5 files changed, 56 insertions(+), 41 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt index 331a3eae..3e4956d5 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 @@ -573,7 +573,6 @@ class MediaService : MediaSessionService(){ fun removeAll() { smPlayer?.stop() smPlayer?.clearMediaItems() - playerChangeNotifier?.notifyStateChange(STATE_IDLE) } 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 6b255b87..2a18bb79 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,12 +1,14 @@ 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 -> PlayerState.IDLE STATE_BUFFERING -> PlayerState.BUFFERING @@ -32,14 +34,6 @@ 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 Notify ==> notifyItemTransition | FROM: $from") channelManager.notifyItemTransition("sua-musica-player") 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 index 69ae8d21..842f891d 100644 --- 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 @@ -100,15 +100,15 @@ class PlayerSwitcher( } override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - if(lastState != STATE_BUFFERING) { + super.onIsPlayingChanged(isPlaying) + if (lastState != STATE_BUFFERING) { playerChangeNotifier?.notifyPlaying(isPlaying) } - if (isPlaying) { - startTrackingProgress() - } else { - stopTrackingProgress() - } + if (isPlaying) { + startTrackingProgress() + } else { + stopTrackingProgress() + } } override fun onMediaItemTransition( @@ -116,26 +116,46 @@ class PlayerSwitcher( reason: @Player.MediaItemTransitionReason Int ) { super.onMediaItemTransition(mediaItem, reason) - val hasItems = (currentPlayer.mediaItemCount ?: 0) > 0 +// oldTransition(mediaItem, reason) + newTransition(reason) + } - val shouldNotify = (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && hasItems) && !PlayerSingleton.shouldNotifyTransition - Log.d(TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason | shouldNotify: $shouldNotify") + fun newTransition( + reason: @Player.MediaItemTransitionReason Int + ) { + val shouldNotify = + reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == MEDIA_ITEM_TRANSITION_REASON_AUTO - if (shouldNotify) { - return - } + Log.d( + TAG, + "#NATIVE LOGS ==> onMediaItemTransition reason: $reason | shouldNotNotify: $shouldNotify" + ) - if (hasItems) { - playerChangeNotifier?.currentMediaIndex( - currentIndex(), - "onMediaItemTransition", - ) + if (!shouldNotify) { + return } + + playerChangeNotifier?.currentMediaIndex( + currentIndex(), + "onMediaItemTransition", + ) mediaButtonEventHandler.buildIcons() playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: $reason | shouldNotifyTransition: ${PlayerSingleton.shouldNotifyTransition}") -// PlayerSingleton.shouldNotifyTransition = false + } + + fun oldTransition( + mediaItem: MediaItem?, + reason: @Player.MediaItemTransitionReason Int + ) { + super.onMediaItemTransition(mediaItem, reason) + Log.d(TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason") + mediaButtonEventHandler.buildIcons() + //TODO: verificar o motivo, tinha um, mas não lembro + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED || !PlayerSingleton.shouldNotifyTransition) { + return + } } var lastState = PlaybackStateCompat.STATE_NONE - 1 @@ -150,6 +170,7 @@ class PlayerSwitcher( if (playbackState == STATE_ENDED) { stopTrackingProgressAndPerformTask {} } + Log.d(TAG, "##onPlaybackStateChanged $playbackState") } @@ -191,14 +212,17 @@ class PlayerSwitcher( } fun currentIndex(): Int { - val position = if (currentPlayer.shuffleModeEnabled) { - PlayerSingleton.shuffledIndices.indexOf( + if ((currentPlayer.mediaItemCount) > 0) { + val position = if (currentPlayer.shuffleModeEnabled) { + PlayerSingleton.shuffledIndices.indexOf( + currentPlayer.currentMediaItemIndex + ) + } else { currentPlayer.currentMediaItemIndex - ) - } else { - currentPlayer.currentMediaItemIndex + } + return position } - return position + return 0 } private fun stopTrackingProgress() { diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 5ede242b..65d84fac 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -182,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; diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 5f3fba8b..6cf3f54e 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -177,8 +177,6 @@ class Player { 'playerId': playerId, 'shallSendEvents': _shallSendEvents, 'externalplayback': externalPlayback, - //If the batch has more than one item, we don't need to notify - //the transition, because it will be notified when the first item is played 'shouldNotifyTransition': batch.length > 1 ? false : shouldNotifyTransition, if (i == 0) ...{ From 295c90ed4b365dca8798a55781b11bd6d3bd44da Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 21 Jan 2025 14:20:47 -0300 Subject: [PATCH 14/34] remove shouldTransition argument --- .../player/MediaButtonEventHandler.kt | 1 - .../br/com/suamusica/player/MediaService.kt | 27 ++++++------------- .../player/MediaSessionConnection.kt | 12 ++++----- .../br/com/suamusica/player/PlayerPlugin.kt | 3 --- .../com/suamusica/player/PlayerSingleton.kt | 2 +- .../br/com/suamusica/player/PlayerSwitcher.kt | 22 +++++---------- packages/player/lib/src/player.dart | 4 --- 7 files changed, 22 insertions(+), 49 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt index edbfeb6c..571b56a5 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 @@ -262,7 +262,6 @@ class MediaButtonEventHandler( mediaService.enqueue( mediaList, args.getBoolean("autoPlay"), - args.getBoolean("shouldNotifyTransition"), ) } return Futures.immediateFuture( diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt index 3e4956d5..dfc2e35d 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt @@ -44,6 +44,7 @@ import br.com.suamusica.player.PlayerPlugin.Companion.SEEK_METHOD 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.MediaLoadRequestData import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.framework.CastContext import com.google.common.collect.ImmutableList @@ -62,7 +63,7 @@ import java.util.Collections const val NOW_PLAYING_NOTIFICATION: Int = 0xb339 @UnstableApi -class MediaService : MediaSessionService(){ +class MediaService : MediaSessionService() { private val TAG = "MediaService" private val userAgent = "SuaMusica/player (Linux; Android ${Build.VERSION.SDK_INT}; ${Build.BRAND}/${Build.MODEL})" @@ -83,7 +84,6 @@ class MediaService : MediaSessionService(){ private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader private lateinit var mediaButtonEventHandler: MediaButtonEventHandler private var shuffleOrder: DefaultShuffleOrder? = null - private var seekToLoadOnly: Boolean = false private var autoPlay: Boolean = true private val channel = Channel>(Channel.BUFFERED) private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) @@ -313,7 +313,7 @@ class MediaService : MediaSessionService(){ fun toggleShuffle(positionsList: List>) { //TODO: AJUSTAR COM CAST - if(smPlayer is CastPlayer){ + if (smPlayer is CastPlayer) { player?.remoteMediaClient?.queueShuffle(JSONObject()) val queue = player?.remoteMediaClient?.mediaQueue val items = queue?.getItemAtIndex(0) @@ -359,10 +359,8 @@ class MediaService : MediaSessionService(){ fun enqueue( medias: List, autoPlay: Boolean, - shouldNotifyTransition: Boolean, ) { this.autoPlay = autoPlay - PlayerSingleton.shouldNotifyTransition = shouldNotifyTransition if (smPlayer?.mediaItemCount == 0) { smPlayer?.playWhenReady = autoPlay } @@ -382,9 +380,6 @@ class MediaService : MediaSessionService(){ player?.addMediaSources(mediaSources) smPlayer?.prepare() } - if (autoPlay) { - playerChangeNotifier?.notifyItemTransition("#NATIVE LOGS MEDIA SERVICE ==> Enqueue - createMediaSource") - } } private fun prepare(cookie: String, media: Media, urlToPrepare: String): MediaSource { @@ -533,7 +528,7 @@ class MediaService : MediaSessionService(){ fun play() { // PlayerSingleton.performAndEnableTracking { - if (smPlayer?.playbackState == STATE_IDLE ) { + if (smPlayer?.playbackState == STATE_IDLE) { smPlayer?.prepare() } smPlayer?.play() @@ -551,22 +546,16 @@ class MediaService : MediaSessionService(){ fun playFromQueue(position: Int, timePosition: Long, loadOnly: Boolean = false) { smPlayer?.playWhenReady = !loadOnly - - if (loadOnly) { - seekToLoadOnly = true - } - + PlayerSingleton.shouldNotifyTransition = smPlayer?.playWhenReady ?: false smPlayer?.seekTo( if (smPlayer?.shuffleModeEnabled == true) PlayerSingleton.shuffledIndices[position] else position, timePosition, ) - - if(position > 0) { - playerChangeNotifier?.notifyPositionChange(timePosition, smPlayer?.duration ?: 0) - } + // if (timePosition > 0) { + // playerChangeNotifier?.notifyPositionChange(timePosition, smPlayer?.duration ?: 0) + // } if (!loadOnly) { smPlayer?.prepare() - playerChangeNotifier?.notifyItemTransition("playFromQueue") } } 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 b8f591f1..c929fc2e 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 @@ -67,11 +67,10 @@ 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_METHOD, bundle) } @@ -87,7 +86,7 @@ class MediaSessionConnection( sendCommand("play", null) } - fun cast(id: String){ + fun cast(id: String) { val bundle = Bundle() bundle.putString("cast_id", id) sendCommand("cast", bundle) @@ -126,16 +125,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) } 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 47911055..ac2d56a6 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 @@ -133,13 +133,10 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { val listMedia: List> = batch["batch"] as List> val autoPlay: Boolean = (batch["autoPlay"] ?: false) as Boolean - val shouldNotifyTransition: Boolean = - (batch["shouldNotifyTransition"] ?: false) as Boolean val json = Gson().toJson(listMedia) PlayerSingleton.mediaSessionConnection?.enqueue( json, autoPlay, - shouldNotifyTransition, ) } "cast" -> { 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 10371daa..0fb116ff 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 @@ -10,7 +10,7 @@ object PlayerSingleton { private const val TAG = "Player" var playerChangeNotifier: PlayerChangeNotifier? = null - var shouldNotifyTransition: Boolean = true + var shouldNotifyTransition: Boolean = false var shuffledIndices = mutableListOf() 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 index 842f891d..f1dc56c7 100644 --- 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 @@ -130,11 +130,15 @@ class PlayerSwitcher( TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason | shouldNotNotify: $shouldNotify" ) - + //We not notify when playFromQueue is loadOnly if (!shouldNotify) { return } - + + if(!PlayerSingleton.shouldNotifyTransition){ + return + } + playerChangeNotifier?.currentMediaIndex( currentIndex(), "onMediaItemTransition", @@ -143,19 +147,7 @@ class PlayerSwitcher( mediaButtonEventHandler.buildIcons() playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: $reason | shouldNotifyTransition: ${PlayerSingleton.shouldNotifyTransition}") - } - - fun oldTransition( - mediaItem: MediaItem?, - reason: @Player.MediaItemTransitionReason Int - ) { - super.onMediaItemTransition(mediaItem, reason) - Log.d(TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason") - mediaButtonEventHandler.buildIcons() - //TODO: verificar o motivo, tinha um, mas não lembro - if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED || !PlayerSingleton.shouldNotifyTransition) { - return - } + PlayerSingleton.shouldNotifyTransition = true } var lastState = PlaybackStateCompat.STATE_NONE - 1 diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 6cf3f54e..15041be0 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -31,7 +31,6 @@ class Player { await enqueueAll( items, alreadyAddedToStorage: true, - shouldNotifyTransition: false, ); }, ); @@ -141,7 +140,6 @@ class Player { bool autoPlay = false, bool saveOnTop = false, bool alreadyAddedToStorage = false, - bool shouldNotifyTransition = false, }) async { if (!alreadyAddedToStorage) { _queue.addAll(items, saveOnTop: saveOnTop); @@ -177,8 +175,6 @@ class Player { 'playerId': playerId, 'shallSendEvents': _shallSendEvents, 'externalplayback': externalPlayback, - 'shouldNotifyTransition': - batch.length > 1 ? false : shouldNotifyTransition, if (i == 0) ...{ 'cookie': cookie, }, From 3dd19366ab0782fd6796102d4a55740909f090a9 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 23 Jan 2025 10:13:37 -0300 Subject: [PATCH 15/34] workaround to use queue in cast --- .../player/MediaButtonEventHandler.kt | 106 ++++++-- .../br/com/suamusica/player/MediaService.kt | 226 +++++++----------- .../player/MediaSessionConnection.kt | 6 + .../player/MethodChannelManagerArgsBuilder.kt | 2 - .../suamusica/player/PlayerChangeNotifier.kt | 4 +- .../br/com/suamusica/player/PlayerPlugin.kt | 6 + .../com/suamusica/player/PlayerSingleton.kt | 17 ++ .../br/com/suamusica/player/PlayerSwitcher.kt | 34 +-- packages/player/lib/src/event_type.dart | 1 + packages/player/lib/src/player.dart | 47 +++- 10 files changed, 256 insertions(+), 193 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaButtonEventHandler.kt index 571b56a5..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 @@ -43,6 +48,7 @@ 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 @@ -96,6 +102,7 @@ class MediaButtonEventHandler( 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 = @@ -127,7 +134,6 @@ class MediaButtonEventHandler( } if (customCommand.customAction == "cast") { -// mediaService.cast(args.getString("cast_id")) mediaService.castWithCastPlayer(args.getString("cast_id")) } @@ -136,10 +142,10 @@ class MediaButtonEventHandler( } if (customCommand.customAction == SEEK_METHOD) { - mediaService.seek( - args.getLong("position"), - args.getBoolean("playWhenReady"), - ) + 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) @@ -170,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) @@ -182,41 +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") { - mediaService.play() + 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_ARGUMENT - ), - ) + 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() + if (session.player is CastPlayer) { + PlayerSingleton.getPreviousMedia() } else { - session.player.seekToPrevious() + if (session.player.hasPreviousMediaItem()) { + session.player.seekToPreviousMediaItem() + } else { + session.player.seekToPrevious() + } } } if (customCommand.customAction == "notification_next" || customCommand.customAction == "next") { - 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) { @@ -289,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 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 dfc2e35d..537190c6 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,7 +1,6 @@ package br.com.suamusica.player import PlayerSwitcher -import android.app.ActivityManager import android.app.PendingIntent import android.app.Service import android.content.Intent @@ -17,8 +16,6 @@ import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata 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.Log import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.Util @@ -40,11 +37,9 @@ import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService 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.SEEK_METHOD 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.MediaLoadRequestData import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.framework.CastContext import com.google.common.collect.ImmutableList @@ -78,9 +73,11 @@ class MediaService : MediaSessionService() { .setUsage(C.USAGE_MEDIA) .build() - var player: PlayerSwitcher? = null + var playerSwitcher: PlayerSwitcher? = null var exoPlayer: ExoPlayer? = null + var castPlayer: CastPlayer? = null + private lateinit var dataSourceBitmapLoader: DataSourceBitmapLoader private lateinit var mediaButtonEventHandler: MediaButtonEventHandler private var shuffleOrder: DefaultShuffleOrder? = null @@ -92,7 +89,7 @@ class MediaService : MediaSessionService() { private var cast: CastManager? = null private var castContext: CastContext? = null - private val smPlayer get() = player?.wrappedPlayer + val smPlayer get() = exoPlayer override fun onCreate() { super.onCreate() @@ -188,7 +185,7 @@ class MediaService : MediaSessionService() { } }) } - player = PlayerSwitcher(exoPlayer!!, mediaButtonEventHandler) + playerSwitcher = PlayerSwitcher(exoPlayer!!, mediaButtonEventHandler) castContext?.let { cast = CastManager(it, this) } @@ -199,33 +196,38 @@ class MediaService : MediaSessionService() { cast?.disconnect() return } - val items = player?.getAllMediaItems() - cast?.connectToCast(castId!!) - var castPlayer: CastPlayer? - cast?.setOnConnectCallback { - val index = smPlayer?.currentMediaItemIndex ?: 0 - val currentPosition: Long = smPlayer?.currentPosition ?: 0 - castPlayer = CastPlayer(castContext!!, CustomMediaItemConverter()) - mediaSession.player = castPlayer!! - player?.setCurrentPlayer(castPlayer!!,castContext?.sessionManager?.currentCastSession?.remoteMediaClient) - if (items != null) { - smPlayer?.setMediaItems(items, index, currentPosition) + 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 - player?.setCurrentPlayer(it) - smPlayer?.prepare() - smPlayer?.seekTo(index, currentPosition) + 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") @@ -301,7 +303,7 @@ class MediaService : MediaSessionService() { val media = smPlayer?.getMediaItemAt(index) media?.associatedMedia?.let { smPlayer?.removeMediaItem(index) - player?.addMediaSource( + smPlayer?.addMediaSource( index, prepare( cookie, it, @@ -312,31 +314,26 @@ class MediaService : MediaSessionService() { } fun toggleShuffle(positionsList: List>) { - //TODO: AJUSTAR COM CAST - if (smPlayer is CastPlayer) { - player?.remoteMediaClient?.queueShuffle(JSONObject()) - val queue = player?.remoteMediaClient?.mediaQueue - val items = queue?.getItemAtIndex(0) - return - } - smPlayer?.shuffleModeEnabled = !(smPlayer?.shuffleModeEnabled ?: false) - smPlayer?.shuffleModeEnabled?.let { - if (it) { - PlayerSingleton.shuffledIndices.clear() - for (e in positionsList) { - PlayerSingleton.shuffledIndices.add(e["originalPosition"] ?: 0) + if (mediaSession.player !is CastPlayer) { + smPlayer?.shuffleModeEnabled = !(smPlayer?.shuffleModeEnabled ?: false) + smPlayer?.shuffleModeEnabled?.let { + if (it) { + PlayerSingleton.shuffledIndices.clear() + for (e in positionsList) { + PlayerSingleton.shuffledIndices.add(e["originalPosition"] ?: 0) + } + shuffleOrder = DefaultShuffleOrder( + PlayerSingleton.shuffledIndices.toIntArray(), + System.currentTimeMillis() + ) + Log.d( + TAG, + "toggleShuffle - shuffledIndices: ${PlayerSingleton.shuffledIndices.size}" + ) + smPlayer?.setShuffleOrder(shuffleOrder!!) } - shuffleOrder = DefaultShuffleOrder( - PlayerSingleton.shuffledIndices.toIntArray(), - System.currentTimeMillis() - ) - Log.d( - TAG, - "toggleShuffle - shuffledIndices: ${PlayerSingleton.shuffledIndices.size} - ${player?.mediaItemCount}" - ) - player?.setShuffleOrder(shuffleOrder!!) + playerChangeNotifier?.onShuffleModeEnabled(it) } - playerChangeNotifier?.onShuffleModeEnabled(it) } } @@ -366,7 +363,7 @@ class MediaService : MediaSessionService() { } Log.d( TAG, - "#NATIVE LOGS MEDIA SERVICE ==> enqueue $autoPlay | mediaItemCount: ${player?.mediaItemCount}" + "#NATIVE LOGS MEDIA SERVICE ==> enqueue $autoPlay | mediaItemCount: ${smPlayer?.mediaItemCount}" ) addToQueue(medias) } @@ -377,11 +374,22 @@ class MediaService : MediaSessionService() { for (i in medias.indices) { mediaSources.add(prepare(cookie, medias[i], "")) } - player?.addMediaSources(mediaSources) + 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) @@ -389,21 +397,16 @@ 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() - .setMediaId(media.id.toString()) - .setUri(uri) - .setMediaMetadata(metadata) - .setMediaId(media.id.toString()) - .setMimeType("audio/mpeg") - .build() - mediaItem.associatedMedia = media + + val mediaItem = createMediaItem(media, uri) + return when (@C.ContentType val type = Util.inferContentType(uri)) { C.CONTENT_TYPE_HLS -> { HlsMediaSource.Factory(dataSourceFactory) @@ -434,30 +437,18 @@ class MediaService : MediaSessionService() { newIndex: Int, positionsList: List> ) { - //TODO: Criei issue no media3 pq esta crashando no reorder do cast - if(smPlayer is CastPlayer){ - val mediaItems = player?.remoteMediaClient?.mediaQueue - val mediaIds = mediaItems?.itemIds - val reorderedIds = mediaIds?.toMutableList() - if (reorderedIds != null) { - val item = reorderedIds.removeAt(oldIndex) - reorderedIds.add(newIndex, item) - } - - player?.remoteMediaClient?.queueReorderItems(reorderedIds?.toIntArray()!!, newIndex, JSONObject()) - return - } - - if (smPlayer?.shuffleModeEnabled == true) { - val list = PlayerSingleton.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 { - smPlayer?.moveMediaItem(oldIndex, newIndex) } } @@ -465,7 +456,7 @@ class MediaService : MediaSessionService() { val sortedIndexes = indexes.sortedDescending() if (sortedIndexes.isNotEmpty()) { sortedIndexes.forEach { - player?.removeMediaItem(it) + smPlayer?.removeMediaItem(it) if (PlayerSingleton.shuffledIndices.isNotEmpty()) { PlayerSingleton.shuffledIndices.removeAt( PlayerSingleton.shuffledIndices.indexOf( @@ -475,12 +466,12 @@ class MediaService : MediaSessionService() { } } } - if (player?.shuffleModeEnabled == true) { + if (smPlayer?.shuffleModeEnabled == true) { shuffleOrder = DefaultShuffleOrder( PlayerSingleton.shuffledIndices.toIntArray(), System.currentTimeMillis() ) - player?.setShuffleOrder(shuffleOrder!!) + smPlayer?.setShuffleOrder(shuffleOrder!!) } } @@ -488,24 +479,6 @@ class MediaService : MediaSessionService() { smPlayer?.repeatMode = REPEAT_MODE_OFF } - fun repeatMode() { - smPlayer?.let { - when (it.repeatMode) { - REPEAT_MODE_OFF -> { - it.repeatMode = REPEAT_MODE_ALL - } - - REPEAT_MODE_ONE -> { - it.repeatMode = REPEAT_MODE_OFF - } - - else -> { - it.repeatMode = REPEAT_MODE_ONE - } - } - } - } - private fun buildMetaData(media: Media): MediaMetadata { val metadataBuilder = MediaMetadata.Builder() @@ -526,24 +499,6 @@ class MediaService : MediaSessionService() { return metadata } - fun play() { -// PlayerSingleton.performAndEnableTracking { - if (smPlayer?.playbackState == STATE_IDLE) { - smPlayer?.prepare() - } - smPlayer?.play() -// } - } - - fun setRepeatMode(mode: String) { - smPlayer?.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) { smPlayer?.playWhenReady = !loadOnly PlayerSingleton.shouldNotifyTransition = smPlayer?.playWhenReady ?: false @@ -551,9 +506,7 @@ class MediaService : MediaSessionService() { if (smPlayer?.shuffleModeEnabled == true) PlayerSingleton.shuffledIndices[position] else position, timePosition, ) - // if (timePosition > 0) { - // playerChangeNotifier?.notifyPositionChange(timePosition, smPlayer?.duration ?: 0) - // } + if (!loadOnly) { smPlayer?.prepare() } @@ -564,28 +517,11 @@ class MediaService : MediaSessionService() { smPlayer?.clearMediaItems() } - fun seek(position: Long, playWhenReady: Boolean) { smPlayer?.seekTo(position) smPlayer?.playWhenReady = playWhenReady } - fun pause() { - smPlayer?.pause() - } - - fun stop() { - smPlayer?.stop() - } - - fun togglePlayPause() { - if (smPlayer?.isPlaying == true) { - pause() - } else { - play() - } - } - private fun releaseAndPerformAndDisableTracking() { 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 c929fc2e..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 @@ -92,6 +92,12 @@ class MediaSessionConnection( sendCommand("cast", bundle) } + fun setCastMedia(media: String) { + val bundle = Bundle() + bundle.putString("media", media) + sendCommand("cast_next_media", bundle) + } + fun setRepeatMode(mode: String) { val bundle = Bundle() bundle.putString("mode", mode) 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/PlayerChangeNotifier.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerChangeNotifier.kt index 2a18bb79..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 @@ -46,11 +46,11 @@ class PlayerChangeNotifier(private val channelManager: MethodChannelManager) { 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/PlayerPlugin.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerPlugin.kt index ac2d56a6..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 @@ -143,6 +143,12 @@ class PlayerPlugin : MethodCallHandler, FlutterPlugin, ActivityAware { 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 -> { PlayerSingleton.mediaSessionConnection?.play() } 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 0fb116ff..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 @@ -48,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/PlayerSwitcher.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/PlayerSwitcher.kt index f1dc56c7..14fb9d42 100644 --- 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 @@ -33,6 +33,9 @@ class PlayerSwitcher( private val TAG = "PlayerSwitcher" private var progressTracker: ProgressTracker? = null var remoteMediaClient: RemoteMediaClient? = null + private var playerState: PlayerState? = null + + var oldPlayer: Player? = null init { playerEventListener?.let { currentPlayer.removeListener(it) } @@ -43,13 +46,14 @@ class PlayerSwitcher( if (this.currentPlayer === newPlayer) { return } + oldPlayer = currentPlayer this.remoteMediaClient = remoteMediaClient - val playerState = savePlayerState() + playerState = savePlayerState() playerEventListener?.let { currentPlayer.removeListener(it) } stopAndClearCurrentPlayer() this.currentPlayer = newPlayer if (currentPlayer is CastPlayer) { - restorePlayerState(playerState) + restorePlayerState(playerState!!) } setupPlayerListener() } @@ -130,12 +134,17 @@ class PlayerSwitcher( TAG, "#NATIVE LOGS ==> onMediaItemTransition reason: $reason | shouldNotNotify: $shouldNotify" ) + + if (currentPlayer is CastPlayer && reason == MEDIA_ITEM_TRANSITION_REASON_AUTO) { + PlayerSingleton.getNextMedia() + } + //We not notify when playFromQueue is loadOnly if (!shouldNotify) { return } - if(!PlayerSingleton.shouldNotifyTransition){ + if (!PlayerSingleton.shouldNotifyTransition) { return } @@ -147,6 +156,7 @@ class PlayerSwitcher( mediaButtonEventHandler.buildIcons() playerChangeNotifier?.notifyItemTransition("onMediaItemTransition reason: $reason | shouldNotifyTransition: ${PlayerSingleton.shouldNotifyTransition}") + PlayerSingleton.shouldNotifyTransition = true } @@ -163,6 +173,11 @@ class PlayerSwitcher( stopTrackingProgressAndPerformTask {} } + if (playbackState == STATE_READY && currentPlayer is CastPlayer) { + currentPlayer.repeatMode = REPEAT_MODE_ALL + playerChangeNotifier?.onRepeatChanged(currentPlayer.repeatMode) + } + Log.d(TAG, "##onPlaybackStateChanged $playbackState") } @@ -222,17 +237,6 @@ class PlayerSwitcher( progressTracker = null } - fun customShuffleModeEnabled(shuffleModeEnabled: Boolean) { - when (currentPlayer) { - is ExoPlayer -> { - (currentPlayer as ExoPlayer).shuffleModeEnabled = shuffleModeEnabled - } - - is CastPlayer -> { - currentPlayer.shuffleModeEnabled = shuffleModeEnabled - } - } - } fun setShuffleOrder(shuffleOrder: ShuffleOrder) { if (currentPlayer is ExoPlayer) { @@ -264,7 +268,7 @@ class PlayerSwitcher( private fun notifyPositionChange() { val position = currentPlayer.currentPosition.coerceAtMost(currentPlayer.duration ?: 0L) - val duration = currentPlayer.duration ?: 0L + val duration = currentPlayer.duration playerChangeNotifier?.notifyPositionChange(position, duration) } } diff --git a/packages/player/lib/src/event_type.dart b/packages/player/lib/src/event_type.dart index ef272629..28716e4e 100644 --- a/packages/player/lib/src/event_type.dart +++ b/packages/player/lib/src/event_type.dart @@ -39,6 +39,7 @@ enum EventType { STATE_ENDED, IDLE, STATE_READY, + CAST_NEXT_MEDIA, } enum PlayerErrorType { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 15041be0..9d80d901 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -680,15 +680,35 @@ class Player { favorite ? EventType.FAVORITE_MUSIC : EventType.UNFAVORITE_MUSIC, "", ); - + break; + case 'cast.mediaFromQueue': + final index = callArgs['index']; + _channel.invokeMethod('cast_next_media', player.items[index].toJson()); + _updateQueueIndexAndNotify( + player: player, + index: index, + ); + break; + case 'cast.nextMedia': + case 'cast.previousMedia': + final media = call.method == 'cast.nextMedia' + ? _queue.possibleNext(_repeatMode) + : _queue.possiblePrevious(); + if (media != null) { + _channel.invokeMethod( + 'cast_next_media', + media.toJson(), + ); + _updateQueueIndexAndNotify( + player: player, + index: player.items.indexOf(media), + ); + } break; case 'SET_CURRENT_MEDIA_INDEX': - _queue.setIndex = callArgs['CURRENT_MEDIA_INDEX']; - _queue.updateIsarIndex(currentMedia!.id, _queue.index); - _notifyPlayerStateChangeEvent( - player, - EventType.SET_CURRENT_MEDIA_INDEX, - "", + _updateQueueIndexAndNotify( + player: player, + index: callArgs['CURRENT_MEDIA_INDEX'], ); break; case 'REPEAT_CHANGED': @@ -866,4 +886,17 @@ class Player { } await Future.wait(futures); } + + static void _updateQueueIndexAndNotify({ + required Player player, + required index, + }) { + _queue.setIndex = index; + _queue.updateIsarIndex(player.currentMedia!.id, _queue.index); + _notifyPlayerStateChangeEvent( + player, + EventType.SET_CURRENT_MEDIA_INDEX, + "", + ); + } } From 127728ceacac182ca014d30415c449fa8bf245fd Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 23 Jan 2025 11:42:59 -0300 Subject: [PATCH 16/34] fix shuffle --- .../br/com/suamusica/player/MediaService.kt | 36 ++++++++++--------- .../example/.flutter-plugins-dependencies | 2 +- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt index 537190c6..0fee0224 100644 --- a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt +++ b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt @@ -314,26 +314,28 @@ class MediaService : MediaSessionService() { } fun toggleShuffle(positionsList: List>) { - if (mediaSession.player !is CastPlayer) { - smPlayer?.shuffleModeEnabled = !(smPlayer?.shuffleModeEnabled ?: false) - smPlayer?.shuffleModeEnabled?.let { - if (it) { - PlayerSingleton.shuffledIndices.clear() - for (e in positionsList) { - PlayerSingleton.shuffledIndices.add(e["originalPosition"] ?: 0) + smPlayer?.shuffleModeEnabled = !(smPlayer?.shuffleModeEnabled ?: false) + smPlayer?.shuffleModeEnabled?.let { + if (it) { + PlayerSingleton.shuffledIndices.clear() + for (e in positionsList) { + PlayerSingleton.shuffledIndices.add(e["originalPosition"] ?: 0) + } + shuffleOrder = DefaultShuffleOrder( + PlayerSingleton.shuffledIndices.toIntArray(), + System.currentTimeMillis() + ) + Log.d( + TAG, + "toggleShuffle - shuffledIndices: ${PlayerSingleton.shuffledIndices.size}" + ) + if(mediaSession.player !is CastPlayer){ + shuffleOrder?.let { shuffleOrder -> + smPlayer?.setShuffleOrder(shuffleOrder) } - shuffleOrder = DefaultShuffleOrder( - PlayerSingleton.shuffledIndices.toIntArray(), - System.currentTimeMillis() - ) - Log.d( - TAG, - "toggleShuffle - shuffledIndices: ${PlayerSingleton.shuffledIndices.size}" - ) - smPlayer?.setShuffleOrder(shuffleOrder!!) } - playerChangeNotifier?.onShuffleModeEnabled(it) } + playerChangeNotifier?.onShuffleModeEnabled(it) } } diff --git a/packages/player/example/.flutter-plugins-dependencies b/packages/player/example/.flutter-plugins-dependencies index ca9c0226..b14d2245 100644 --- a/packages/player/example/.flutter-plugins-dependencies +++ b/packages/player/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"android":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_android-2.2.10/","native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs"]}],"macos":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"isar_flutter_libs","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider"]}],"date_created":"2024-09-14 19:45:21.353811","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":true,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"mdns_plugin","path":"/Users/lucastonussi/.pub-cache/git/flutter-6ce9697a9c0a20c7224fd75562164bfb91e1e372/mdns_plugin/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs","file_picker","mdns_plugin"]}],"android":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_plugin_android_lifecycle","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.23/","native_build":true,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"mdns_plugin","path":"/Users/lucastonussi/.pub-cache/git/flutter-6ce9697a9c0a20c7224fd75562164bfb91e1e372/mdns_plugin/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_android-2.2.10/","native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs","file_picker","mdns_plugin"]}],"macos":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]}],"web":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","dependencies":[]}]},"dependencyGraph":[{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"isar_flutter_libs","dependencies":[]},{"name":"mdns_plugin","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider","file_picker","mdns_plugin"]}],"date_created":"2025-01-23 10:06:21.123741","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file From ed1fa15c87038fc4d6ab2aa1bf95bd65acb6facc Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 19 May 2025 17:38:01 -0300 Subject: [PATCH 17/34] wip --- .../br/com/suamusica/player/PlayerSwitcher.kt | 17 +++++------ .../player/ios/Classes/PlayerPlugin.swift | 3 +- packages/player/ios/Classes/PlayerState.swift | 2 +- packages/player/ios/Classes/SMPlayer.swift | 23 ++++----------- .../ios/Classes/SMPlayerListeners.swift | 29 +++++++++++++++---- packages/player/lib/src/player.dart | 2 +- packages/player/lib/src/queue.dart | 10 +++++-- 7 files changed, 45 insertions(+), 41 deletions(-) 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 index 14fb9d42..4103f8d0 100644 --- 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 @@ -132,26 +132,23 @@ class PlayerSwitcher( Log.d( TAG, - "#NATIVE LOGS ==> onMediaItemTransition reason: $reason | shouldNotNotify: $shouldNotify" + "#NATIVE LOGS ==> onMediaItemTransition reason: $reason | shouldNotNotify: $shouldNotify | shouldNotifyTransition ${PlayerSingleton.shouldNotifyTransition}" ) if (currentPlayer is CastPlayer && reason == MEDIA_ITEM_TRANSITION_REASON_AUTO) { PlayerSingleton.getNextMedia() } - //We not notify when playFromQueue is loadOnly - if (!shouldNotify) { - return - } + playerChangeNotifier?.currentMediaIndex( + currentIndex(), + "onMediaItemTransition", + ) + //We not notify when playFromQueue is loadOnly if (!PlayerSingleton.shouldNotifyTransition) { return } - playerChangeNotifier?.currentMediaIndex( - currentIndex(), - "onMediaItemTransition", - ) mediaButtonEventHandler.buildIcons() @@ -229,7 +226,7 @@ class PlayerSwitcher( } return position } - return 0 + return -1 } private fun stopTrackingProgress() { diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index 9bb1c75d..20b2dd0e 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -28,10 +28,9 @@ public class PlayerPlugin: NSObject, FlutterPlugin { let listMedia = batch["batch"] as? [[String: Any]] { let autoPlay = batch["autoPlay"] as? Bool ?? false let cookie = batch["cookie"] as? String ?? "" - let shouldNotifyTransition = batch["shouldNotifyTransition"] as? Bool ?? false if let mediaList = convertToMedia(mediaArray: listMedia) { MessageBuffer.shared.send(mediaList) - smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie, shouldNotifyTransition: shouldNotifyTransition) + smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie) } } result(NSNumber(value: true)) diff --git a/packages/player/ios/Classes/PlayerState.swift b/packages/player/ios/Classes/PlayerState.swift index 9bf7c195..5fe97534 100644 --- a/packages/player/ios/Classes/PlayerState.swift +++ b/packages/player/ios/Classes/PlayerState.swift @@ -18,6 +18,6 @@ enum PlayerState: Int { case seekEnd case bufferEmpty case itemTransition - case stateEnded case stateReady + case stateEnded } diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 00c62261..adf5f423 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -17,9 +17,8 @@ public class SMPlayer : NSObject { private var isShuffleModeEnabled: Bool = false var shuffledQueue: [AVPlayerItem] = [] private var listeners: SMPlayerListeners? = nil - private var seekToLoadOnly: Bool = false // Transition Control - private var shouldNotifyTransition: Bool = false + private var shouldNotifyTransition: Bool = true var areNotificationCommandsEnabled: Bool = true var fullQueue: [AVPlayerItem] { @@ -38,7 +37,6 @@ public class SMPlayer : NSObject { super.init() self.methodChannelManager = methodChannelManager listeners = SMPlayerListeners(smPlayer:smPlayer,methodChannelManager:methodChannelManager) - listeners?.addPlayerObservers() NotificationCenter.default.addObserver( self, @@ -54,12 +52,8 @@ public class SMPlayer : NSObject { } shouldNotifyTransition = true self.updateEndPlaybackObserver() - seekToLoadOnly = !seekToLoadOnly self.listeners?.addItemsObservers() - if(seekToLoadOnly){ - seekToLoadOnly = false methodChannelManager?.currentMediaIndex(index: self.currentIndex) - } } } setupNowPlayingInfoCenter() @@ -139,6 +133,7 @@ public class SMPlayer : NSObject { smPlayer.pause() smPlayer.replaceCurrentItem(with: nil) clearNowPlayingInfo() + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.idle) } func clearNowPlayingInfo() { @@ -146,10 +141,9 @@ public class SMPlayer : NSObject { removeNotification() } - func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String, shouldNotifyTransition: Bool) { + func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { var playerItem: AVPlayerItem? guard let message = MessageBuffer.shared.receive() else { return } - self.shouldNotifyTransition = shouldNotifyTransition if(!cookie.isEmpty){ self.cookie = cookie } @@ -172,11 +166,8 @@ public class SMPlayer : NSObject { self.setNowPlaying() self.enableCommands() } - print("#ENQUEUE: shouldNotifyTransition: \(shouldNotifyTransition)") - if(shouldNotifyTransition){ - methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) - } self.enableCommands() + listeners?.addPlayerObservers() } func removeByPosition(indexes: [Int]) { @@ -383,11 +374,6 @@ public class SMPlayer : NSObject { } func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false) { - if (loadOnly) { - seekToLoadOnly = true - listeners?.mediaChange?.invalidate() - } - listeners?.removeItemObservers() distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position, completionHandler: { print("#NATIVE LOGS ==> completionHandler") self.methodChannelManager?.currentMediaIndex(index: self.currentIndex) @@ -397,6 +383,7 @@ public class SMPlayer : NSObject { }) if(loadOnly){ pause() + shouldNotifyTransition = false }else{ play() } diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index 3ce59162..398968af 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -22,23 +22,33 @@ public class SMPlayerListeners : NSObject { private var notPlayingReason: NSKeyValueObservation? private var playback: NSKeyValueObservation? + private var lastState = PlayerState.idle + func addItemsObservers() { removeItemObservers() guard let currentItem = smPlayer.currentItem else { return } statusChange = currentItem.observe(\.status, options: [.new, .old]) { (playerItem, change) in - if playerItem.status == .failed { - if let error = playerItem.error { + switch playerItem.status { + case .failed: + if let error = playerItem.error { print("#NATIVE LOGS ==> ERROR: \(String(describing: playerItem.error))") self.methodChannelManager?.notifyError(error: "UNKNOW ERROR") } + case .readyToPlay: + self.notifyPlayerStateChange(state: PlayerState.stateReady) + case .unknown: + break + @unknown default: + break } } + loading = currentItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old]) { [weak self] (new, old) in guard let self = self else { return } print("#NATIVE LOGS ==> Listeners - observer - loading") - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + notifyPlayerStateChange(state: PlayerState.buffering) } loaded = currentItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { (player, _) in @@ -46,6 +56,13 @@ public class SMPlayerListeners : NSObject { } } + func notifyPlayerStateChange(state: PlayerState){ + if(lastState != state){ + self.methodChannelManager?.notifyPlayerStateChange(state: state) + lastState = state + } + } + func addMediaChangeObserver(){ mediaChange = smPlayer.observe(\.currentItem, options: [.new, .old]) { [weak self] (player, change) in @@ -92,13 +109,13 @@ public class SMPlayerListeners : NSObject { guard let self = self else { return } switch player.timeControlStatus { case .playing: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.playing) + notifyPlayerStateChange(state: PlayerState.playing) print("#NATIVE LOGS ==> Listeners - Playing") case .paused: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.paused) + notifyPlayerStateChange(state: PlayerState.paused) print("#NATIVE LOGS ==> Listeners - Paused") case .waitingToPlayAtSpecifiedRate: - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.buffering) + notifyPlayerStateChange(state: PlayerState.buffering) print("#NATIVE LOGS ==> Listeners - Buffering") @unknown default: break diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index 9d80d901..cb8e0b81 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -870,7 +870,7 @@ class Player { static void _addUsingPlayer(Player player, Event event) { if (event.type != EventType.POSITION_CHANGE) { - debugPrint("_platformCallHandler _addUsingPlayer $event"); + debugPrint("_platformCallHandler _addUsingPlayer ${event.type}"); } if (!player._eventStreamController.isClosed && (player._shallSendEvents || diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index 0e4adb61..13167e90 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -50,10 +50,14 @@ class Queue { Media? _current; set setIndex(int index) { - if (storage.isNotEmpty && index >= 0 && index <= storage.length - 1) { - _index = index; - _current = storage[index].item; + if (storage.isEmpty || index < 0 || index >= storage.length) { + _index = -1; + _current = null; + return; } + + _index = index; + _current = storage[index].item; } final Shuffler _shuffler; From 09e5bad7742b2e49846661aed4d66eb329c35e55 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 27 May 2025 16:00:59 -0300 Subject: [PATCH 18/34] wip --- .../example/.flutter-plugins-dependencies | 2 +- .../Flutter/ephemeral/flutter_lldb_helper.py | 32 +++++++++++++++++++ .../ios/Flutter/ephemeral/flutter_lldbinit | 5 +++ packages/player/pubspec.yaml | 1 - 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 packages/player/example/ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 packages/player/example/ios/Flutter/ephemeral/flutter_lldbinit diff --git a/packages/player/example/.flutter-plugins-dependencies b/packages/player/example/.flutter-plugins-dependencies index b14d2245..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":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":true,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"mdns_plugin","path":"/Users/lucastonussi/.pub-cache/git/flutter-6ce9697a9c0a20c7224fd75562164bfb91e1e372/mdns_plugin/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs","file_picker","mdns_plugin"]}],"android":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_plugin_android_lifecycle","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.23/","native_build":true,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"mdns_plugin","path":"/Users/lucastonussi/.pub-cache/git/flutter-6ce9697a9c0a20c7224fd75562164bfb91e1e372/mdns_plugin/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_android-2.2.10/","native_build":true,"dependencies":[]},{"name":"smplayer","path":"/Users/lucastonussi/SM/flutter_plugins/packages/player/","native_build":true,"dependencies":["isar_flutter_libs","file_picker","mdns_plugin"]}],"macos":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","native_build":false,"dependencies":[]},{"name":"isar_flutter_libs","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/lucastonussi/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]}],"web":[{"name":"file_picker","path":"/Users/lucastonussi/.pub-cache/git/flutter_file_picker-528cd01a317b45305d293c03e855ec7255c3ab59/","dependencies":[]}]},"dependencyGraph":[{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"isar_flutter_libs","dependencies":[]},{"name":"mdns_plugin","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"smplayer","dependencies":["isar_flutter_libs","path_provider","file_picker","mdns_plugin"]}],"date_created":"2025-01-23 10:06:21.123741","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file +{"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/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/pubspec.yaml b/packages/player/pubspec.yaml index 387666c4..d8df4152 100644 --- a/packages/player/pubspec.yaml +++ b/packages/player/pubspec.yaml @@ -19,7 +19,6 @@ dependencies: file_picker: git: url: https://github.com/SuaMusica/flutter_file_picker.git - ref: 528cd01a317b45305d293c03e855ec7255c3ab59 smaws: git: url: https://github.com/SuaMusica/flutter_plugins.git From 1873b1e6da5661980742e2697d8f8128aaa7b1ae Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 28 May 2025 14:45:58 -0300 Subject: [PATCH 19/34] improves and reorganizing --- packages/player/example/lib/sm_player.dart | 8 +- packages/player/lib/player.dart | 1 - .../player/lib/src/before_play_event.dart | 8 +- .../player/lib/src/duration_change_event.dart | 8 +- packages/player/lib/src/isar_service.dart | 58 +- packages/player/lib/src/media.dart | 152 +- .../player/lib/src/network_change_event.dart | 13 +- packages/player/lib/src/player.dart | 935 +++------ packages/player/lib/src/player_channel.dart | 324 ++++ .../lib/src/player_event_controller.dart | 50 + .../player/lib/src/position_change_event.dart | 7 +- .../lib/src/previous_playlist_model.dart | 5 +- .../lib/src/previous_playlist_model.g.dart | 1680 ++++++++++------- packages/player/lib/src/queue.dart | 274 +-- packages/player/lib/src/queue_item.dart | 6 +- packages/player/lib/src/release_mode.dart | 5 - packages/player/lib/src/repeat_mode.dart | 2 - packages/player/pubspec.yaml | 2 +- packages/player/test/player_test.dart | 311 +-- packages/player/test/queue_test.dart | 238 ++- 20 files changed, 2252 insertions(+), 1835 deletions(-) create mode 100644 packages/player/lib/src/player_channel.dart create mode 100644 packages/player/lib/src/player_event_controller.dart delete mode 100644 packages/player/lib/src/release_mode.dart diff --git a/packages/player/example/lib/sm_player.dart b/packages/player/example/lib/sm_player.dart index 65d84fac..00eba612 100644 --- a/packages/player/example/lib/sm_player.dart +++ b/packages/player/example/lib/sm_player.dart @@ -224,19 +224,19 @@ class _SMPlayerState extends State { if (_player.state == PlayerState.STATE_READY) { 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) { + if (result == Player.ok) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Audio is now playing again!!!!'))); } @@ -362,7 +362,7 @@ class _SMPlayerState extends State { title: Text('Remove all'), onTap: () { _player.stop(); - _player.removeAll(); + _player.removeAllMedias(); Navigator.pop(context); }, ), diff --git a/packages/player/lib/player.dart b/packages/player/lib/player.dart index 4aeb277f..6491426f 100644 --- a/packages/player/lib/player.dart +++ b/packages/player/lib/player.dart @@ -1,6 +1,5 @@ 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'; diff --git a/packages/player/lib/src/before_play_event.dart b/packages/player/lib/src/before_play_event.dart index 9db10729..96beb44c 100644 --- a/packages/player/lib/src/before_play_event.dart +++ b/packages/player/lib/src/before_play_event.dart @@ -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/duration_change_event.dart b/packages/player/lib/src/duration_change_event.dart index a3ae60e1..5b9a0cbe 100644 --- a/packages/player/lib/src/duration_change_event.dart +++ b/packages/player/lib/src/duration_change_event.dart @@ -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/isar_service.dart b/packages/player/lib/src/isar_service.dart index 804e1aa4..cb6ecb88 100644 --- a/packages/player/lib/src/isar_service.dart +++ b/packages/player/lib/src/isar_service.dart @@ -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/lib/src/media.dart b/packages/player/lib/src/media.dart index ce6be301..1b61d813 100644 --- a/packages/player/lib/src/media.dart +++ b/packages/player/lib/src/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/network_change_event.dart b/packages/player/lib/src/network_change_event.dart index 6699cc25..1195763f 100644 --- a/packages/player/lib/src/network_change_event.dart +++ b/packages/player/lib/src/network_change_event.dart @@ -2,10 +2,7 @@ import 'package:smplayer/src/event.dart'; import 'package:smplayer/src/event_type.dart'; import 'package:smplayer/src/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/player.dart b/packages/player/lib/src/player.dart index cb8e0b81..c2480b94 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -2,17 +2,16 @@ 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 'package:smplayer/src/player_channel.dart'; +import 'package:smplayer/src/player_event_controller.dart'; import 'player_state.dart'; @@ -25,55 +24,41 @@ class Player { this.autoPlay = false, }) { _queue = Queue( - beforeInitialize: () async => await _channel.invokeMethod('remove_all'), + beforeInitialize: () async => + await _playerChannel.invokeMethod('remove_all'), initializeIsar: this.initializeIsar, - onInitialize: () async { - await enqueueAll( - items, - alreadyAddedToStorage: true, - ); + onInitialize: (List items) async { + await enqueueAll(items, alreadyAddedToStorage: true); }, ); - player = this; + eventController = PlayerEventController(); } - 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 variables 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(); + static const ok = PlayerChannel.ok; + static const notOk = PlayerChannel.notOk; + // Required constructor parameters 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; + + // 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, @@ -85,56 +70,33 @@ class Player { EventType.PLAYING, EventType.EXTERNAL_RESUME_REQUESTED, EventType.EXTERNAL_PAUSE_REQUESTED, - EventType.SET_CURRENT_MEDIA_INDEX + 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; + // 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; - return _channel - .invokeMethod(method, args) - .then((result) => result ?? Future.value(Ok)); - } + // ================ 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; - set setQueuePosition(int position) { + // ================ Queue Management Methods ================ + set queuePosition(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 cast(String castId) async { - await _channel.invokeMethod('cast', {'castId': castId}); - return Ok; - } - Future enqueueAll( List items, { bool autoPlay = false, @@ -144,207 +106,133 @@ class Player { 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(); + 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( - _channel.invokeMethod( - 'enqueue', - { - 'batch': batch, - 'autoPlay': autoPlay, - 'playerId': playerId, - 'shallSendEvents': _shallSendEvents, - 'externalplayback': externalPlayback, - if (i == 0) ...{ - 'cookie': cookie, - }, - }, - ), + _playerChannel.invokeMethod('enqueue', { + 'batch': batch, + 'autoPlay': autoPlay, + 'playerId': playerId, + 'shallSendEvents': shallSendEvents, + 'externalplayback': externalPlayback, + if (i == 0) ...{'cookie': cookie}, + }), ); } - return Ok; + return PlayerChannel.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}); - + int removeByPosition({required List positionsToDelete}) { + _playerChannel.invokeMethod('remove_in', { + 'indexesToDelete': positionsToDelete, + }); return _queue.removeByPosition( positionsToDelete: positionsToDelete, isShuffle: isShuffleEnabled, ); } - Future removeAll() async { + Future removeAllMedias() async { _queue.clear(); - setQueuePosition = 0; + queuePosition = 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; + _playerChannel.invokeMethod('remove_all'); + return PlayerChannel.ok; } - Future reorder( - int oldIndex, - int newIndex, - ) async { + Future reorder(int oldIndex, int newIndex) async { _queue.reorder(oldIndex, newIndex, isShuffleEnabled); - debugPrint('#_queue.reorder: ${getPositionsList()}'); - _channel.invokeMethod('reorder', { + debugPrint('#queue.reorder: ${getPositionsList()}'); + _playerChannel.invokeMethod('reorder', { 'oldIndex': oldIndex, 'newIndex': newIndex, 'positionsList': getPositionsList(), }); - return Ok; + return PlayerChannel.ok; } - Future clear() async => removeAll(); + Future clear() async => removeAllMedias(); - 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 restartQueue() async { + final media = _queue.restart(); + return media; + } + // ================ Playback Control Methods ================ Future play() async { - await _invokeMethod('play'); - return Ok; + await _invokeMethodWithDefaultArgs('play'); + return PlayerChannel.ok; } - Future disableNotificatonCommands() async { - await _invokeMethod('disable_notification_commands'); - return Ok; + Future pause() async { + _notifyPlayerStatusChangeEvent(EventType.PAUSE_REQUEST); + return await _invokeMethodWithDefaultArgs('pause'); } - Future enableNotificatonCommands() async { - await _invokeMethod('enable_notification_commands'); - return Ok; - } + Future stop() async { + _notifyPlayerStatusChangeEvent(EventType.STOP_REQUESTED); + final int result = await _invokeMethodWithDefaultArgs('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"); + if (result == PlayerChannel.ok) { + state = PlayerState.STOPPED; + _notifyPlayerStatusChangeEvent(EventType.STOPPED); } - 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, - } - ]; + return result; } - Future forward() async { - if (currentMedia == null) { - return NotOk; + Future release() async { + _notifyPlayerStatusChangeEvent(EventType.RELEASE_REQUESTED); + final int result = await _invokeMethodWithDefaultArgs('release'); + + if (result == PlayerChannel.ok) { + state = PlayerState.STOPPED; + _notifyPlayerStatusChangeEvent(EventType.RELEASED); } - return _forward(currentMedia); + _queue.dispose(); + return result; } - 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 seek(Duration position, {bool playWhenReady = true}) { + _notifyPlayerStateChangeEvent(this, EventType.SEEK_START, ""); + return _invokeMethodWithDefaultArgs('seek', { + 'position': position.inMilliseconds, + 'playWhenReady': playWhenReady, + }); } - Future toggleRepeatMode() async { - return _channel.invokeMethod('repeat_mode').then((result) => result); + Future setVolume(double volume) { + return _invokeMethodWithDefaultArgs('setVolume', {'volume': volume}); } - Future setRepeatMode(String mode) async { - return _channel.invokeMethod( - 'set_repeat_mode', {'mode': mode}).then((result) => result); + Future getDuration() { + return _invokeMethodWithDefaultArgs('getDuration'); } - Future disableRepeatMode() async { - return _channel - .invokeMethod('disable_repeat_mode') - .then((result) => result); + 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 Ok; + return PlayerChannel.ok; } Media? media = _queue.possiblePrevious(); @@ -357,7 +245,7 @@ class Player { if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { setRepeatMode("all"); } - return await _invokeMethod('previous'); + return await _invokeMethodWithDefaultArgs('previous'); } Future next({bool isFromChromecast = false}) async { @@ -369,51 +257,58 @@ class Player { if (repeatMode == RepeatMode.REPEAT_MODE_ONE) { setRepeatMode("all"); } - return _invokeMethod('next'); + return _invokeMethodWithDefaultArgs('next'); } else { return null; } } - Future updateFavorite({ - required bool isFavorite, - required int id, - }) async { - return _channel.invokeMethod('update_favorite', { - 'isFavorite': isFavorite, - 'idFavorite': id, - }).then((result) => result); + Future forward() async { + if (currentMedia == null) { + return PlayerChannel.notOk; + } + return _forward(currentMedia); } - Future pause() async { - _notifyPlayerStatusChangeEvent(EventType.PAUSE_REQUEST); - return await _invokeMethod('pause'); + 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(); } - 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); + 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, + }); + } - return result; + // ================ Repeat and Shuffle Methods ================ + Future toggleRepeatMode() async { + return _playerChannel.invokeMethod('repeat_mode'); } - Future release() async { - _notifyPlayerStatusChangeEvent(EventType.RELEASE_REQUESTED); - final int result = await _invokeMethod('release'); + Future setRepeatMode(String mode) async { + return _playerChannel.invokeMethod('set_repeat_mode', {'mode': mode}); + } - if (result == Ok) { - state = PlayerState.STOPPED; - _notifyPlayerStatusChangeEvent(EventType.RELEASED); - } - _queue.dispose(); - return result; + Future disableRepeatMode() async { + return _playerChannel.invokeMethod('disable_repeat_mode'); } Future toggleShuffle() async { @@ -422,368 +317,119 @@ class Player { } else { _queue.unshuffle(); } - debugPrint('#_queue.shuffle: ${getPositionsList()}'); - _channel - .invokeMethod('toggle_shuffle', {'positionsList': getPositionsList()}); + debugPrint('#queue.shuffle: ${getPositionsList()}'); + _playerChannel.invokeMethod('toggle_shuffle', { + 'positionsList': getPositionsList(), + }); } - Future seek( - Duration position, { - bool playWhenReady = true, - }) { - _notifyPlayerStateChangeEvent(this, EventType.SEEK_START, ""); - return _invokeMethod('seek', { - 'position': position.inMilliseconds, - 'playWhenReady': playWhenReady, - }); + // ================ Notification Methods ================ + Future removeNotification() async { + await _playerChannel.invokeMethod('remove_notification'); + return PlayerChannel.ok; } - Future setVolume(double volume) { - return _invokeMethod('setVolume', {'volume': volume}); + Future disableNotificatonCommands() async { + await _invokeMethodWithDefaultArgs('disable_notification_commands'); + return PlayerChannel.ok; } - Future getDuration() { - return _invokeMethod('getDuration'); + Future enableNotificatonCommands() async { + await _invokeMethodWithDefaultArgs('enable_notification_commands'); + return PlayerChannel.ok; } - Future getCurrentPosition() async { - return _invokeMethod('getCurrentPosition'); + Future notifyAdsPlaying() async { + await _invokeMethodWithDefaultArgs('ads_playing'); + return PlayerChannel.ok; } - static Future platformCallHandler(MethodCall call) async { - try { - _doHandlePlatformCall(call); - } catch (ex) { - _log('Unexpected error: $ex'); - } + // ================ Event Management Methods ================ + int enableEvents() { + shallSendEvents = true; + return PlayerChannel.ok; } - static Future _handleOnComplete(Player player) async { - player.state = PlayerState.COMPLETED; - _notifyPlayerStateChangeEvent(player, EventType.FINISHED_PLAYING, ""); + int disableEvents() { + shallSendEvents = false; + return PlayerChannel.ok; } - 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: - _notifyPlayerStateChangeEvent( - player, - EventType.STATE_READY, - error, - ); - break; - 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 'cast.mediaFromQueue': - final index = callArgs['index']; - _channel.invokeMethod('cast_next_media', player.items[index].toJson()); - _updateQueueIndexAndNotify( - player: player, - index: index, - ); - break; - case 'cast.nextMedia': - case 'cast.previousMedia': - final media = call.method == 'cast.nextMedia' - ? _queue.possibleNext(_repeatMode) - : _queue.possiblePrevious(); - if (media != null) { - _channel.invokeMethod( - 'cast_next_media', - media.toJson(), - ); - _updateQueueIndexAndNotify( - player: player, - index: player.items.indexOf(media), - ); - } - break; - case 'SET_CURRENT_MEDIA_INDEX': - _updateQueueIndexAndNotify( - player: player, - index: callArgs['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} '); - } + // ================ Media Management Methods ================ + Future updateMediaUri({required int id, String? uri}) async { + _playerChannel.invokeMethod('update_media_uri', {'id': id, 'uri': uri}); + return PlayerChannel.ok; } - // _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(); + Future updateFavorite({ + required bool isFavorite, + required int id, + }) async { + return _playerChannel.invokeMethod('update_favorite', { + 'isFavorite': isFavorite, + 'idFavorite': id, + }); + } - _add(Event( - type: EventType.FORWARD, - media: media, - queuePosition: currentIndex, - position: Duration(milliseconds: positionInMilli), - duration: Duration(milliseconds: durationInMilli), - )); + Future cast(String castId) async { + await _playerChannel.invokeMethod('cast', {'castId': castId}); + return PlayerChannel.ok; } - _notifyPlayerStatusChangeEvent(EventType type) { - if (currentMedia != null) { - _add( - Event( - type: type, - media: currentMedia!, - queuePosition: currentIndex, - ), - ); - } + // ================ Queue State Methods ================ + Media? possibleNext(RepeatMode repeatMode) { + return _queue.possibleNext(repeatMode); + } + + Media? possiblePrevious() { + return _queue.possiblePrevious(); } - static _notifyDurationChangeEvent(Player player, Duration newDuration) { + void setIndexAndUpdateIsar(int index) { + _queue.setIndex = index; + _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 (_queue.current != null) { - _addUsingPlayer( - player, - DurationChangeEvent( - media: _queue.current!, - queuePosition: currentIndex, - duration: newDuration)); + 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(), + ), + ), + ); + } } } - static _notifyPlayerStateChangeEvent( + void _notifyPlayerStateChangeEvent( Player player, EventType eventType, String error, @@ -797,8 +443,7 @@ class Player { ); } if (_queue.current != null) { - _addUsingPlayer( - player, + eventController.add( Event( type: eventType, media: _queue.current!, @@ -808,15 +453,14 @@ class Player { } } - static _notifyPlayerErrorEvent({ + void _notifyPlayerErrorEvent({ required Player player, required String error, PlayerErrorType? errorType, }) { final currentIndex = _queue.index; if (_queue.current != null) { - _addUsingPlayer( - player, + eventController.add( Event( type: EventType.ERROR_OCCURED, media: _queue.current!, @@ -828,75 +472,52 @@ class Player { } } - 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, - ), + 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), ); - 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) { + // ================ Utility Methods ================ + void _log(String param) { debugPrint(param); } - void _add(Event event) { - if (!_eventStreamController.isClosed && - (_shallSendEvents || chromeCastEnabledEvents.contains(event.type))) { - _eventStreamController.add(event); + 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; - static void _addUsingPlayer(Player player, Event event) { - if (event.type != EventType.POSITION_CHANGE) { - debugPrint("_platformCallHandler _addUsingPlayer ${event.type}"); - } - if (!player._eventStreamController.isClosed && - (player._shallSendEvents || - player.chromeCastEnabledEvents.contains(event.type))) { - player._eventStreamController.add(event); - } + return _playerChannel.invokeMethod(method, args); } Future dispose() async { - List futures = []; - if (!_eventStreamController.isClosed) { - futures.add(_eventStreamController.close()); - } - await Future.wait(futures); - } - - static void _updateQueueIndexAndNotify({ - required Player player, - required index, - }) { - _queue.setIndex = index; - _queue.updateIsarIndex(player.currentMedia!.id, _queue.index); - _notifyPlayerStateChangeEvent( - player, - EventType.SET_CURRENT_MEDIA_INDEX, - "", - ); + await eventController.dispose(); + _queue.dispose(); } } diff --git a/packages/player/lib/src/player_channel.dart b/packages/player/lib/src/player_channel.dart new file mode 100644 index 00000000..3cc3f6a0 --- /dev/null +++ b/packages/player/lib/src/player_channel.dart @@ -0,0 +1,324 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; +import 'package:smplayer/src/event.dart'; +import 'package:smplayer/src/event_type.dart'; +import 'package:smplayer/src/player.dart'; +import 'package:smplayer/src/player_state.dart'; +import 'package:smplayer/src/repeat_mode.dart'; +import 'package:smplayer/src/duration_change_event.dart'; +import 'package:smplayer/src/position_change_event.dart'; +import 'package:smplayer/src/isar_service.dart'; +import 'package:smplayer/src/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 { + arguments ??= const {}; + return _channel + .invokeMethod(method, arguments) + .then((result) => result ?? Future.value(ok)); + } + + 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/player_event_controller.dart b/packages/player/lib/src/player_event_controller.dart new file mode 100644 index 00000000..757512c7 --- /dev/null +++ b/packages/player/lib/src/player_event_controller.dart @@ -0,0 +1,50 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:smplayer/src/event.dart'; +import 'package:smplayer/src/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/position_change_event.dart b/packages/player/lib/src/position_change_event.dart index bfa7f5b0..a3e6776e 100644 --- a/packages/player/lib/src/position_change_event.dart +++ b/packages/player/lib/src/position_change_event.dart @@ -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/previous_playlist_model.dart b/packages/player/lib/src/previous_playlist_model.dart index 34dcdb2b..1407b657 100644 --- a/packages/player/lib/src/previous_playlist_model.dart +++ b/packages/player/lib/src/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/previous_playlist_model.g.dart b/packages/player/lib/src/previous_playlist_model.g.dart index f2380e7c..ba9e71aa 100644 --- a/packages/player/lib/src/previous_playlist_model.g.dart +++ b/packages/player/lib/src/previous_playlist_model.g.dart @@ -22,7 +22,7 @@ const PreviousPlaylistMusicsSchema = CollectionSchema( id: 0, name: r'musics', type: IsarType.stringList, - ) + ), }, estimateSize: _previousPlaylistMusicsEstimateSize, serialize: _previousPlaylistMusicsSerialize, @@ -100,39 +100,53 @@ Id _previousPlaylistMusicsGetId(PreviousPlaylistMusics object) { } List> _previousPlaylistMusicsGetLinks( - PreviousPlaylistMusics object) { + PreviousPlaylistMusics object, +) { return []; } void _previousPlaylistMusicsAttach( - IsarCollection col, Id id, PreviousPlaylistMusics object) { + IsarCollection col, + Id id, + PreviousPlaylistMusics object, +) { object.id = id; } extension PreviousPlaylistMusicsQueryWhereSort on QueryBuilder { QueryBuilder - anyId() { + anyId() { return QueryBuilder.apply(this, (query) { return query.addWhereClause(const IdWhereClause.any()); }); } } -extension PreviousPlaylistMusicsQueryWhere on QueryBuilder< - PreviousPlaylistMusics, PreviousPlaylistMusics, QWhereClause> { - QueryBuilder idEqualTo(Id id) { +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, - )); + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); }); } - QueryBuilder idNotEqualTo(Id id) { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterWhereClause + > + idNotEqualTo(Id id) { return QueryBuilder.apply(this, (query) { if (query.whereSort == Sort.asc) { return query @@ -154,8 +168,12 @@ extension PreviousPlaylistMusicsQueryWhere on QueryBuilder< }); } - QueryBuilder idGreaterThan(Id id, {bool include = 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), @@ -163,8 +181,12 @@ extension PreviousPlaylistMusicsQueryWhere on QueryBuilder< }); } - QueryBuilder idLessThan(Id id, {bool include = false}) { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterWhereClause + > + idLessThan(Id id, {bool include = false}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( IdWhereClause.lessThan(upper: id, includeUpper: include), @@ -172,148 +194,201 @@ extension PreviousPlaylistMusicsQueryWhere on QueryBuilder< }); } - QueryBuilder idBetween( + 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, - )); + 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, - )); +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 idGreaterThan( - Id value, { - bool include = false, - }) { + 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, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); }); } - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { + 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, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); }); } - QueryBuilder idBetween( + 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, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } - QueryBuilder musicsIsNull() { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsIsNull() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'musics', - )); + return query.addFilterCondition( + const FilterCondition.isNull(property: r'musics'), + ); }); } - QueryBuilder musicsIsNotNull() { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsIsNotNull() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'musics', - )); + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'musics'), + ); }); } - QueryBuilder musicsElementEqualTo( - String value, { - bool caseSensitive = true, - }) { + 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, - )); + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder musicsElementGreaterThan( + 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, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder musicsElementLessThan( + 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, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder musicsElementBetween( + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementBetween( String lower, String upper, { bool includeLower = true, @@ -321,162 +396,174 @@ extension PreviousPlaylistMusicsQueryFilter on QueryBuilder< 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, - )); + 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, - }) { + 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, - )); + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder musicsElementEndsWith( - String value, { - bool caseSensitive = true, - }) { + 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, - )); + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder - musicsElementContains(String value, {bool caseSensitive = true}) { + 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, - )); + return query.addFilterCondition( + FilterCondition.contains( + property: r'musics', + value: value, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder - musicsElementMatches(String pattern, {bool caseSensitive = true}) { + 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, - )); + return query.addFilterCondition( + FilterCondition.matches( + property: r'musics', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); }); } - QueryBuilder musicsElementIsEmpty() { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementIsEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'musics', - value: '', - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'musics', value: ''), + ); }); } - QueryBuilder musicsElementIsNotEmpty() { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsElementIsNotEmpty() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'musics', - value: '', - )); + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'musics', value: ''), + ); }); } - QueryBuilder musicsLengthEqualTo(int length) { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsLengthEqualTo(int length) { return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - length, - true, - length, - true, - ); + return query.listLength(r'musics', length, true, length, true); }); } - QueryBuilder musicsIsEmpty() { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsIsEmpty() { return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - 0, - true, - 0, - true, - ); + return query.listLength(r'musics', 0, true, 0, true); }); } - QueryBuilder musicsIsNotEmpty() { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsIsNotEmpty() { return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - 0, - false, - 999999, - true, - ); + return query.listLength(r'musics', 0, false, 999999, true); }); } - QueryBuilder musicsLengthLessThan( - int length, { - bool include = false, - }) { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsLengthLessThan(int length, {bool include = false}) { return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - 0, - true, - length, - include, - ); + return query.listLength(r'musics', 0, true, length, include); }); } - QueryBuilder musicsLengthGreaterThan( - int length, { - bool include = false, - }) { + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsLengthGreaterThan(int length, {bool include = false}) { return QueryBuilder.apply(this, (query) { - return query.listLength( - r'musics', - length, - include, - 999999, - true, - ); + return query.listLength(r'musics', length, include, 999999, true); }); } - QueryBuilder musicsLengthBetween( + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QAfterFilterCondition + > + musicsLengthBetween( int lower, int upper, { bool includeLower = true, @@ -494,26 +581,41 @@ extension PreviousPlaylistMusicsQueryFilter on QueryBuilder< } } -extension PreviousPlaylistMusicsQueryObject on QueryBuilder< - PreviousPlaylistMusics, PreviousPlaylistMusics, QFilterCondition> {} - -extension PreviousPlaylistMusicsQueryLinks on QueryBuilder< - PreviousPlaylistMusics, PreviousPlaylistMusics, QFilterCondition> {} +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> { +extension PreviousPlaylistMusicsQuerySortThenBy + on + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QSortThenBy + > { QueryBuilder - thenById() { + thenById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); }); } QueryBuilder - thenByIdDesc() { + thenByIdDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.desc); }); @@ -523,15 +625,20 @@ extension PreviousPlaylistMusicsQuerySortThenBy on QueryBuilder< extension PreviousPlaylistMusicsQueryWhereDistinct on QueryBuilder { QueryBuilder - distinctByMusics() { + distinctByMusics() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'musics'); }); } } -extension PreviousPlaylistMusicsQueryProperty on QueryBuilder< - PreviousPlaylistMusics, PreviousPlaylistMusics, QQueryProperty> { +extension PreviousPlaylistMusicsQueryProperty + on + QueryBuilder< + PreviousPlaylistMusics, + PreviousPlaylistMusics, + QQueryProperty + > { QueryBuilder idProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'id'); @@ -539,7 +646,7 @@ extension PreviousPlaylistMusicsQueryProperty on QueryBuilder< } QueryBuilder?, QQueryOperations> - musicsProperty() { + musicsProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'musics'); }); @@ -551,7 +658,7 @@ extension PreviousPlaylistMusicsQueryProperty on QueryBuilder< extension GetPreviousPlaylistCurrentIndexCollection on Isar { IsarCollection - get previousPlaylistCurrentIndexs => this.collection(); + get previousPlaylistCurrentIndexs => this.collection(); } const PreviousPlaylistCurrentIndexSchema = CollectionSchema( @@ -563,11 +670,7 @@ const PreviousPlaylistCurrentIndexSchema = CollectionSchema( name: r'currentIndex', type: IsarType.long, ), - r'mediaId': PropertySchema( - id: 1, - name: r'mediaId', - type: IsarType.long, - ) + r'mediaId': PropertySchema(id: 1, name: r'mediaId', type: IsarType.long), }, estimateSize: _previousPlaylistCurrentIndexEstimateSize, serialize: _previousPlaylistCurrentIndexSerialize, @@ -637,39 +740,62 @@ Id _previousPlaylistCurrentIndexGetId(PreviousPlaylistCurrentIndex object) { } List> _previousPlaylistCurrentIndexGetLinks( - PreviousPlaylistCurrentIndex object) { + PreviousPlaylistCurrentIndex object, +) { return []; } void _previousPlaylistCurrentIndexAttach( - IsarCollection col, Id id, PreviousPlaylistCurrentIndex object) { + IsarCollection col, + Id id, + PreviousPlaylistCurrentIndex object, +) { object.id = id; } -extension PreviousPlaylistCurrentIndexQueryWhereSort on QueryBuilder< - PreviousPlaylistCurrentIndex, PreviousPlaylistCurrentIndex, QWhere> { - QueryBuilder anyId() { +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 idEqualTo(Id id) { +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, - )); + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); }); } - QueryBuilder idNotEqualTo(Id id) { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterWhereClause + > + idNotEqualTo(Id id) { return QueryBuilder.apply(this, (query) { if (query.whereSort == Sort.asc) { return query @@ -691,8 +817,12 @@ extension PreviousPlaylistCurrentIndexQueryWhere on QueryBuilder< }); } - QueryBuilder idGreaterThan(Id id, {bool include = 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), @@ -700,8 +830,12 @@ extension PreviousPlaylistCurrentIndexQueryWhere on QueryBuilder< }); } - QueryBuilder idLessThan(Id id, {bool include = false}) { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterWhereClause + > + idLessThan(Id id, {bool include = false}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( IdWhereClause.lessThan(upper: id, includeUpper: include), @@ -709,338 +843,472 @@ extension PreviousPlaylistCurrentIndexQueryWhere on QueryBuilder< }); } - QueryBuilder idBetween( + 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, - )); + return query.addWhereClause( + IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + ), + ); }); } } -extension PreviousPlaylistCurrentIndexQueryFilter on QueryBuilder< +extension PreviousPlaylistCurrentIndexQueryFilter + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QFilterCondition + > { + QueryBuilder< PreviousPlaylistCurrentIndex, PreviousPlaylistCurrentIndex, - QFilterCondition> { - QueryBuilder currentIndexIsNull() { + QAfterFilterCondition + > + currentIndexIsNull() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'currentIndex', - )); + return query.addFilterCondition( + const FilterCondition.isNull(property: r'currentIndex'), + ); }); } - QueryBuilder currentIndexIsNotNull() { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + currentIndexIsNotNull() { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'currentIndex', - )); + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'currentIndex'), + ); }); } - QueryBuilder currentIndexEqualTo(int? value) { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + currentIndexEqualTo(int? value) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'currentIndex', - value: value, - )); + 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< + 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 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 + > + 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( + 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, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'currentIndex', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } - QueryBuilder idEqualTo(Id value) { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + idEqualTo(Id value) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); + 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< + 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 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 + > + 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( + 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, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } - QueryBuilder mediaIdEqualTo(int value) { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterFilterCondition + > + mediaIdEqualTo(int value) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'mediaId', - value: value, - )); + 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< + 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 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 + > + 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( + 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, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'mediaId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } } -extension PreviousPlaylistCurrentIndexQueryObject on QueryBuilder< +extension PreviousPlaylistCurrentIndexQueryObject + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QFilterCondition + > {} + +extension PreviousPlaylistCurrentIndexQueryLinks + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QFilterCondition + > {} + +extension PreviousPlaylistCurrentIndexQuerySortBy + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QSortBy + > { + QueryBuilder< PreviousPlaylistCurrentIndex, PreviousPlaylistCurrentIndex, - QFilterCondition> {} - -extension PreviousPlaylistCurrentIndexQueryLinks on QueryBuilder< - PreviousPlaylistCurrentIndex, - PreviousPlaylistCurrentIndex, - QFilterCondition> {} - -extension PreviousPlaylistCurrentIndexQuerySortBy on QueryBuilder< - PreviousPlaylistCurrentIndex, PreviousPlaylistCurrentIndex, QSortBy> { - QueryBuilder sortByCurrentIndex() { + QAfterSortBy + > + sortByCurrentIndex() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'currentIndex', Sort.asc); }); } - QueryBuilder sortByCurrentIndexDesc() { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + sortByCurrentIndexDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'currentIndex', Sort.desc); }); } - QueryBuilder sortByMediaId() { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + sortByMediaId() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'mediaId', Sort.asc); }); } - QueryBuilder sortByMediaIdDesc() { + 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 thenByCurrentIndex() { +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 thenByCurrentIndexDesc() { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + thenByCurrentIndexDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'currentIndex', Sort.desc); }); } - QueryBuilder thenById() { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + thenById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); }); } - QueryBuilder thenByIdDesc() { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + thenByIdDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.desc); }); } - QueryBuilder thenByMediaId() { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QAfterSortBy + > + thenByMediaId() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'mediaId', Sort.asc); }); } - QueryBuilder thenByMediaIdDesc() { + 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 distinctByCurrentIndex() { +extension PreviousPlaylistCurrentIndexQueryWhereDistinct + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QDistinct + > { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QDistinct + > + distinctByCurrentIndex() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'currentIndex'); }); } - QueryBuilder distinctByMediaId() { + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QDistinct + > + distinctByMediaId() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'mediaId'); }); } } -extension PreviousPlaylistCurrentIndexQueryProperty on QueryBuilder< - PreviousPlaylistCurrentIndex, - PreviousPlaylistCurrentIndex, - QQueryProperty> { +extension PreviousPlaylistCurrentIndexQueryProperty + on + QueryBuilder< + PreviousPlaylistCurrentIndex, + PreviousPlaylistCurrentIndex, + QQueryProperty + > { QueryBuilder - idProperty() { + idProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'id'); }); } QueryBuilder - currentIndexProperty() { + currentIndexProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'currentIndex'); }); } QueryBuilder - mediaIdProperty() { + mediaIdProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'mediaId'); }); @@ -1064,16 +1332,12 @@ const PreviousPlaylistPositionSchema = CollectionSchema( name: r'duration', type: IsarType.double, ), - r'mediaId': PropertySchema( - id: 1, - name: r'mediaId', - type: IsarType.long, - ), + 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, @@ -1147,39 +1411,58 @@ Id _previousPlaylistPositionGetId(PreviousPlaylistPosition object) { } List> _previousPlaylistPositionGetLinks( - PreviousPlaylistPosition object) { + PreviousPlaylistPosition object, +) { return []; } void _previousPlaylistPositionAttach( - IsarCollection col, Id id, PreviousPlaylistPosition object) { + IsarCollection col, + Id id, + PreviousPlaylistPosition object, +) { object.id = id; } -extension PreviousPlaylistPositionQueryWhereSort on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QWhere> { +extension PreviousPlaylistPositionQueryWhereSort + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QWhere + > { QueryBuilder - anyId() { + anyId() { return QueryBuilder.apply(this, (query) { return query.addWhereClause(const IdWhereClause.any()); }); } } -extension PreviousPlaylistPositionQueryWhere on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QWhereClause> { - QueryBuilder idEqualTo(Id id) { +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, - )); + return query.addWhereClause(IdWhereClause.between(lower: id, upper: id)); }); } - QueryBuilder idNotEqualTo(Id id) { + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterWhereClause + > + idNotEqualTo(Id id) { return QueryBuilder.apply(this, (query) { if (query.whereSort == Sort.asc) { return query @@ -1201,8 +1484,12 @@ extension PreviousPlaylistPositionQueryWhere on QueryBuilder< }); } - QueryBuilder idGreaterThan(Id id, {bool include = 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), @@ -1210,8 +1497,12 @@ extension PreviousPlaylistPositionQueryWhere on QueryBuilder< }); } - QueryBuilder idLessThan(Id id, {bool include = false}) { + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterWhereClause + > + idLessThan(Id id, {bool include = false}) { return QueryBuilder.apply(this, (query) { return query.addWhereClause( IdWhereClause.lessThan(upper: id, includeUpper: include), @@ -1219,74 +1510,104 @@ extension PreviousPlaylistPositionQueryWhere on QueryBuilder< }); } - QueryBuilder idBetween( + 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, - )); + 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, - )); +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 durationGreaterThan( + 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, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'duration', + value: value, + epsilon: epsilon, + ), + ); }); } - QueryBuilder durationLessThan( + 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, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'duration', + value: value, + epsilon: epsilon, + ), + ); }); } - QueryBuilder durationBetween( + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + durationBetween( double lower, double upper, { bool includeLower = true, @@ -1294,177 +1615,228 @@ extension PreviousPlaylistPositionQueryFilter on QueryBuilder< 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, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'duration', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + epsilon: epsilon, + ), + ); }); } - QueryBuilder idEqualTo(Id value) { + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + idEqualTo(Id value) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'id', value: value), + ); }); } - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { + 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, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + ), + ); }); } - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { + 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, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + ), + ); }); } - QueryBuilder idBetween( + 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, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } - QueryBuilder mediaIdEqualTo(int value) { + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + mediaIdEqualTo(int value) { return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'mediaId', - value: value, - )); + return query.addFilterCondition( + FilterCondition.equalTo(property: r'mediaId', value: value), + ); }); } - QueryBuilder mediaIdGreaterThan( - int value, { - bool include = false, - }) { + 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, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'mediaId', + value: value, + ), + ); }); } - QueryBuilder mediaIdLessThan( - int value, { - bool include = false, - }) { + 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, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'mediaId', + value: value, + ), + ); }); } - QueryBuilder mediaIdBetween( + 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, - )); + return query.addFilterCondition( + FilterCondition.between( + property: r'mediaId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); }); } - QueryBuilder positionEqualTo( - double value, { - double epsilon = Query.epsilon, - }) { + 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, - )); + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'position', + value: value, + epsilon: epsilon, + ), + ); }); } - QueryBuilder positionGreaterThan( + 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, - )); + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'position', + value: value, + epsilon: epsilon, + ), + ); }); } - QueryBuilder positionLessThan( + 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, - )); + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'position', + value: value, + epsilon: epsilon, + ), + ); }); } - QueryBuilder positionBetween( + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QAfterFilterCondition + > + positionBetween( double lower, double upper, { bool includeLower = true, @@ -1472,154 +1844,186 @@ extension PreviousPlaylistPositionQueryFilter on QueryBuilder< 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, - )); + 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> { +extension PreviousPlaylistPositionQueryObject + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QFilterCondition + > {} + +extension PreviousPlaylistPositionQueryLinks + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QFilterCondition + > {} + +extension PreviousPlaylistPositionQuerySortBy + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QSortBy + > { QueryBuilder - sortByDuration() { + sortByDuration() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'duration', Sort.asc); }); } QueryBuilder - sortByDurationDesc() { + sortByDurationDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'duration', Sort.desc); }); } QueryBuilder - sortByMediaId() { + sortByMediaId() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'mediaId', Sort.asc); }); } QueryBuilder - sortByMediaIdDesc() { + sortByMediaIdDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'mediaId', Sort.desc); }); } QueryBuilder - sortByPosition() { + sortByPosition() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'position', Sort.asc); }); } QueryBuilder - sortByPositionDesc() { + sortByPositionDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'position', Sort.desc); }); } } -extension PreviousPlaylistPositionQuerySortThenBy on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QSortThenBy> { +extension PreviousPlaylistPositionQuerySortThenBy + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QSortThenBy + > { QueryBuilder - thenByDuration() { + thenByDuration() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'duration', Sort.asc); }); } QueryBuilder - thenByDurationDesc() { + thenByDurationDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'duration', Sort.desc); }); } QueryBuilder - thenById() { + thenById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); }); } QueryBuilder - thenByIdDesc() { + thenByIdDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.desc); }); } QueryBuilder - thenByMediaId() { + thenByMediaId() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'mediaId', Sort.asc); }); } QueryBuilder - thenByMediaIdDesc() { + thenByMediaIdDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'mediaId', Sort.desc); }); } QueryBuilder - thenByPosition() { + thenByPosition() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'position', Sort.asc); }); } QueryBuilder - thenByPositionDesc() { + thenByPositionDesc() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'position', Sort.desc); }); } } -extension PreviousPlaylistPositionQueryWhereDistinct on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QDistinct> { +extension PreviousPlaylistPositionQueryWhereDistinct + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QDistinct + > { QueryBuilder - distinctByDuration() { + distinctByDuration() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'duration'); }); } QueryBuilder - distinctByMediaId() { + distinctByMediaId() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'mediaId'); }); } QueryBuilder - distinctByPosition() { + distinctByPosition() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'position'); }); } } -extension PreviousPlaylistPositionQueryProperty on QueryBuilder< - PreviousPlaylistPosition, PreviousPlaylistPosition, QQueryProperty> { +extension PreviousPlaylistPositionQueryProperty + on + QueryBuilder< + PreviousPlaylistPosition, + PreviousPlaylistPosition, + QQueryProperty + > { QueryBuilder idProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'id'); @@ -1627,21 +2031,21 @@ extension PreviousPlaylistPositionQueryProperty on QueryBuilder< } QueryBuilder - durationProperty() { + durationProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'duration'); }); } QueryBuilder - mediaIdProperty() { + mediaIdProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'mediaId'); }); } QueryBuilder - positionProperty() { + 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 index 13167e90..aafaf420 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -11,8 +11,7 @@ import 'package:smplayer/src/simple_shuffle.dart'; class Queue { Queue({ - shuffler, - mode, + Shuffler? shuffler, this.initializeIsar = false, this.onInitialize, this.beforeInitialize, @@ -21,28 +20,36 @@ class Queue { 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; - } + 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; + } + } } var _index = 0; @@ -50,112 +57,114 @@ class Queue { Media? _current; set setIndex(int index) { - if (storage.isEmpty || index < 0 || index >= storage.length) { + if (playerQueue.isEmpty || index < 0 || index >= playerQueue.length) { _index = -1; _current = null; return; } _index = index; - _current = storage[index].item; + _current = playerQueue[index].item; } final Shuffler _shuffler; final bool initializeIsar; - final Future Function()? onInitialize, beforeInitialize; + final Future Function(List)? onInitialize; + final Future Function()? beforeInitialize; bool itemsReady = false; int previousIndex = 0; PreviousPlaylistPosition? previousPosition; - var storage = >[]; + List> playerQueue = >[]; PreviousPlaylistMusics? previousPlaylistMusics; DateTime? _lastPrevious; Media? get current => _current ?? - (index >= 0 && index < storage.length ? storage[index].item : null); + (index >= 0 && index < playerQueue.length + ? playerQueue[index].item + : null); List get items { - return storage.length > 0 - ? List.unmodifiable((storage.map((i) => i.item).toList())) + return playerQueue.length > 0 + ? List.unmodifiable((playerQueue.map((i) => i.item).toList())) : []; } Future> get previousItems async { - previousPlaylistMusics = - await IsarService.instance.getPreviousPlaylistMusics(); + previousPlaylistMusics = await IsarService.instance + .getPreviousPlaylistMusics(); return previousPlaylistMusics?.musics?.toListMedia ?? []; } Future get _previousPlaylistPosition async { - final previousPlaylistPosition = - await IsarService.instance.getPreviousPlaylistPosition(); + final previousPlaylistPosition = await IsarService.instance + .getPreviousPlaylistPosition(); return previousPlaylistPosition?.position != null ? previousPlaylistPosition : null; } Future get previousPlaylistIndex async { - final previousPlaylistCurrentIndex = - await IsarService.instance.getPreviousPlaylistCurrentIndex(); + final previousPlaylistCurrentIndex = await IsarService.instance + .getPreviousPlaylistCurrentIndex(); return previousPlaylistCurrentIndex?.currentIndex ?? 0; } - int get size => storage.length; + int get size => playerQueue.length; Media? get top { if (this.size > 0) { - return storage[0].item; + return playerQueue[0].item; } return null; } play(Media media) { - if (storage.length > 0) { - storage.replaceRange(0, 1, [QueueItem(0, 0, media)]); + if (playerQueue.length > 0) { + playerQueue.replaceRange(0, 1, [QueueItem(0, 0, media)]); } else { int pos = _nextPosition(); - storage.add(QueueItem(pos, pos, media)); + playerQueue.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); + if (playerQueue.isNotEmpty && + index > -1 && + index <= (playerQueue.length - 1)) { + playerQueue[index] = playerQueue[index].copyWith(item: media); } } add(Media media) async { int pos = _nextPosition(); - storage.add(QueueItem(pos, pos, media)); + playerQueue.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(); + 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; + addAll(List items, {bool saveOnTop = false}) async { + int i = playerQueue.length == 1 ? 0 : playerQueue.length - 1; if (saveOnTop) { - storage.insertAll(0, _toQueueItems(items, i)); + playerQueue.insertAll(0, _toQueueItems(items, i)); } else { - storage.addAll(_toQueueItems(items, i)); + playerQueue.addAll(_toQueueItems(items, i)); } await _save(medias: items, saveOnTop: saveOnTop); } - Future _save( - {required List medias, bool saveOnTop = false}) async { + Future _save({ + required List medias, + bool saveOnTop = false, + }) async { final items = await previousItems; debugPrint( '[TESTE] itemsFromStorage: ${items.length} - mediasToSave: ${medias.length}', @@ -166,6 +175,39 @@ class Queue { ); } + /// 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, @@ -176,33 +218,36 @@ class Queue { return [ ...topList.toListStringCompressed, - ...bottomList.toListStringCompressed + ...bottomList.toListStringCompressed, ]; } - int removeByPosition( - {required List positionsToDelete, required bool isShuffle}) { + int removeByPosition({ + required List positionsToDelete, + required bool isShuffle, + }) { try { - int lastLength = storage.length; + int lastLength = playerQueue.length; for (var i = 0; i < positionsToDelete.length; ++i) { final pos = positionsToDelete[i] - i; if (pos < index) { setIndex = index - 1; } - storage.removeAt(pos); + playerQueue.removeAt(pos); } - for (var j = 0; j < storage.length; ++j) { - storage[j].position = j; + for (var j = 0; j < playerQueue.length; ++j) { + playerQueue[j].position = j; } if (kDebugMode) { - for (var e in storage) { + for (var e in playerQueue) { debugPrint( - '=====> storage remove: ${e.item.name} - ${e.position} | ${e.originalPosition}'); + '=====> storage remove: ${e.item.name} - ${e.position} | ${e.originalPosition}', + ); } } - return lastLength - storage.length; + return lastLength - playerQueue.length; } catch (e) { return 0; } @@ -211,35 +256,36 @@ class Queue { clear() => removeAll(); removeAll() { - storage.clear(); + playerQueue.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; + if (playerQueue.length > 2) { + var current = playerQueue[index]; + _shuffler.shuffle(playerQueue); + for (var i = 0; i < playerQueue.length; ++i) { + playerQueue[i].position = i; } - var currentIndex = storage.indexOf(current); + var currentIndex = playerQueue.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]; + if (playerQueue.length > 2) { + 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 storage) { + for (var e in playerQueue) { debugPrint( - '=====> storage unshuffle: ${e.item.name} - ${e.position} | ${e.originalPosition}'); + '=====> storage unshuffle: ${e.item.name} - ${e.position} | ${e.originalPosition}', + ); } } setIndex = current.position; @@ -247,8 +293,8 @@ class Queue { } _nextPosition() { - if (storage.length == 0) return 0; - return storage.length; + if (playerQueue.length == 0) return 0; + return playerQueue.length; } bool shouldRewind() { @@ -272,18 +318,20 @@ class Queue { if (index > 0) { --workIndex; } - return storage[workIndex].item; + return playerQueue[workIndex].item; } - return storage.isNotEmpty && index >= 0 ? storage[index].item : null; + return playerQueue.isNotEmpty && index >= 0 + ? playerQueue[index].item + : null; } // Media? next() { - // if (storage.length == 0) { + // if (playerQueue.length == 0) { // throw AssertionError("Queue is empty"); - // } else if (storage.length > 0 && index < storage.length - 1) { + // } else if (playerQueue.length > 0 && index < playerQueue.length - 1) { // final newIndex = index + 1; // setIndex = newIndex; - // var media = storage[newIndex].item; + // var media = playerQueue[newIndex].item; // updateIsarIndex(media.id, newIndex); // return media; // } else { @@ -296,8 +344,8 @@ class Queue { repeatMode == RepeatMode.REPEAT_MODE_ONE) { return _next(); } else if (repeatMode == RepeatMode.REPEAT_MODE_ALL) { - if (storage.length - 1 == index) { - return storage[0].item; + if (playerQueue.length - 1 == index) { + return playerQueue[0].item; } else { return _next(); } @@ -307,10 +355,10 @@ class Queue { } Media? _next() { - if (storage.length == 0) { + if (playerQueue.length == 0) { return null; - } else if (storage.length > 0 && index < storage.length - 1) { - var media = storage[index + 1].item; + } else if (playerQueue.length > 0 && index < playerQueue.length - 1) { + var media = playerQueue[index + 1].item; return media; } else { return null; @@ -318,10 +366,10 @@ class Queue { } Media? move(int pos) { - if (storage.length == 0) { + if (playerQueue.length == 0) { throw AssertionError("Queue is empty"); - } else if (storage.length > 0 && pos <= storage.length - 1) { - var media = storage[pos].item; + } else if (playerQueue.length > 0 && pos <= playerQueue.length - 1) { + var media = playerQueue[pos].item; setIndex = pos; return media; } else { @@ -336,11 +384,11 @@ class Queue { } // Media? item(int pos) { - // final item = storage[pos].item; + // final item = playerQueue[pos].item; // updateIsarIndex(item.id, pos); - // if (storage.length == 0) { + // if (playerQueue.length == 0) { // return null; - // } else if (storage.length > 0 && pos <= storage.length - 1) { + // } else if (playerQueue.length > 0 && pos <= playerQueue.length - 1) { // return item; // } else { // return null; @@ -349,42 +397,42 @@ class Queue { Media restart() { setIndex = 0; - return storage.first.item; + return playerQueue.first.item; } reorder(int oldIndex, int newIndex, [bool isShuffle = false]) { - final playingItem = storage.elementAt(index); + final playingItem = playerQueue.elementAt(index); if (newIndex > oldIndex) { for (int i = oldIndex + 1; i <= newIndex; i++) { if (!isShuffle) { - storage[i].originalPosition--; + playerQueue[i].originalPosition--; } - storage[i].position--; + playerQueue[i].position--; } } else { for (int i = newIndex; i < oldIndex; i++) { if (!isShuffle) { - storage[i].originalPosition++; + playerQueue[i].originalPosition++; } - storage[i].position++; + playerQueue[i].position++; } } - storage[oldIndex].position = newIndex; + playerQueue[oldIndex].position = newIndex; if (!isShuffle) { - storage[oldIndex].originalPosition = newIndex; + playerQueue[oldIndex].originalPosition = newIndex; } - storage.sort((a, b) => a.position.compareTo(b.position)); - final playingIndex = storage.indexOf(playingItem); + playerQueue.sort((a, b) => a.position.compareTo(b.position)); + final playingIndex = playerQueue.indexOf(playingItem); if (kDebugMode) { debugPrint( - '=====> ${storage[oldIndex].item.name} - storage[oldIndex]: ${storage[oldIndex].originalPosition}', + '=====> ${playerQueue[oldIndex].item.name} - playerQueue[oldIndex]: ${playerQueue[oldIndex].originalPosition}', ); debugPrint( - '=====> ${storage[newIndex].item.name} - storage[newIndex]: ${storage[newIndex].originalPosition}', + '=====> ${playerQueue[newIndex].item.name} - playerQueue[newIndex]: ${playerQueue[newIndex].originalPosition}', ); - for (var e in storage) { + for (var e in playerQueue) { debugPrint( '=====> storage Reorder: ${e.item.name} - ${e.position} - ${e.originalPosition}', ); diff --git a/packages/player/lib/src/queue_item.dart b/packages/player/lib/src/queue_item.dart index 7647a178..83540a6a 100644 --- a/packages/player/lib/src/queue_item.dart +++ b/packages/player/lib/src/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/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/repeat_mode.dart b/packages/player/lib/src/repeat_mode.dart index da1d5114..e90369fb 100644 --- a/packages/player/lib/src/repeat_mode.dart +++ b/packages/player/lib/src/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/pubspec.yaml b/packages/player/pubspec.yaml index d8df4152..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 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..e6091a57 100644 --- a/packages/player/test/queue_test.dart +++ b/packages/player/test/queue_test.dart @@ -6,53 +6,56 @@ 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', () { @@ -90,7 +93,9 @@ void main() { subject.add(media3); subject.removeByPosition( - positionsToDelete: [subject.storage[1].position], isShuffle: false); + positionsToDelete: [subject.storage[1].position], + isShuffle: false, + ); expect(subject.size, 2); expect(subject.top, media1); @@ -145,16 +150,10 @@ 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.rewind(), throwsAssertionError); + }); test('Rewind on a queue that was not played shall raise an error', () { final subject = Queue(); @@ -207,60 +206,62 @@ void main() { 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.add(media1); + subject.add(media2); + subject.add(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.next(); + 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.next(); + 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.previous(); + final previous = subject.previous(); + 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.add(media1); + subject.add(media2); + subject.add(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.next(); + 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.next(); + 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.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]); + }, + ); test('Next on empty queue shall raise an error', () { final subject = Queue(); expect(() => subject.next(), throwsAssertionError); @@ -278,27 +279,24 @@ void main() { 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.add(media1); + subject.add(media2); + subject.add(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.next(); + 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.next(); + 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); From 07cb25fbad16c9473f0a01e92ec63e97f9f469f3 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 28 May 2025 15:40:14 -0300 Subject: [PATCH 20/34] remove unused methods --- packages/player/lib/src/queue.dart | 56 -------- packages/player/test/queue_test.dart | 196 +++++++++++++++++---------- 2 files changed, 122 insertions(+), 130 deletions(-) diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index aafaf420..d23bd051 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -118,31 +118,6 @@ class Queue { return null; } - play(Media media) { - if (playerQueue.length > 0) { - playerQueue.replaceRange(0, 1, [QueueItem(0, 0, media)]); - } else { - int pos = _nextPosition(); - playerQueue.add(QueueItem(pos, pos, media)); - } - _save(medias: [media]); - setIndex = 0; - } - - void replaceCurrent(Media media) { - if (playerQueue.isNotEmpty && - index > -1 && - index <= (playerQueue.length - 1)) { - playerQueue[index] = playerQueue[index].copyWith(item: media); - } - } - - add(Media media) async { - int pos = _nextPosition(); - playerQueue.add(QueueItem(pos, pos, media)); - await _save(medias: [media]); - } - List> _toQueueItems(List items, int i) { return items.map((e) { i++; @@ -292,11 +267,6 @@ class Queue { } } - _nextPosition() { - if (playerQueue.length == 0) return 0; - return playerQueue.length; - } - bool shouldRewind() { if (index >= 0) { final now = DateTime.now(); @@ -325,20 +295,6 @@ class Queue { : null; } - // Media? next() { - // if (playerQueue.length == 0) { - // throw AssertionError("Queue is empty"); - // } else if (playerQueue.length > 0 && index < playerQueue.length - 1) { - // final newIndex = index + 1; - // setIndex = newIndex; - // var media = playerQueue[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) { @@ -383,18 +339,6 @@ class Queue { ); } - // Media? item(int pos) { - // final item = playerQueue[pos].item; - // updateIsarIndex(item.id, pos); - // if (playerQueue.length == 0) { - // return null; - // } else if (playerQueue.length > 0 && pos <= playerQueue.length - 1) { - // return item; - // } else { - // return null; - // } - // } - Media restart() { setIndex = 0; return playerQueue.first.item; diff --git a/packages/player/test/queue_test.dart b/packages/player/test/queue_test.dart index e6091a57..89d08756 100644 --- a/packages/player/test/queue_test.dart +++ b/packages/player/test/queue_test.dart @@ -1,6 +1,7 @@ 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'; @@ -60,16 +61,14 @@ void main() { 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]); @@ -77,23 +76,19 @@ 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], + positionsToDelete: [subject.playerQueue[1].position], isShuffle: false, ); @@ -152,26 +147,20 @@ void main() { test('Rewind on empty queue shall raise an error', () { final subject = Queue(); - expect(() => subject.rewind(), throwsAssertionError); + 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); @@ -180,26 +169,21 @@ 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); @@ -209,24 +193,22 @@ void main() { 'Two consecutive previous invocation shall really go the previous track', () { 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]); - subject.previous(); - final previous = subject.previous(); + subject.possiblePrevious(); + final previous = subject.possiblePrevious(); expect(subject.size, 3); expect(subject.current, media2); expect(previous, media2); @@ -237,25 +219,23 @@ void main() { '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); + 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]); - subject.previous(); + subject.possiblePrevious(); sleep(Duration(seconds: 3)); - final previous = subject.previous(); + final previous = subject.possiblePrevious(); expect(subject.size, 3); expect(subject.current, media3); expect(previous, media3); @@ -264,15 +244,16 @@ void main() { ); 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); @@ -281,17 +262,15 @@ void main() { }); 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); + 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); @@ -299,29 +278,27 @@ void main() { }); 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); @@ -335,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); @@ -347,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); + }); + }); } From 3703b26eb719920631d928a7fd44cda3b479be92 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 29 May 2025 11:09:55 -0300 Subject: [PATCH 21/34] Enhance error handling in PlayerSwitcher and improve Queue initialization logic --- .../br/com/suamusica/player/PlayerSwitcher.kt | 77 +++++++--- packages/player/lib/src/player.dart | 4 +- packages/player/lib/src/queue.dart | 138 +++++++++--------- 3 files changed, 126 insertions(+), 93 deletions(-) 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 index 4103f8d0..a608ab8d 100644 --- 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 @@ -35,27 +35,46 @@ class PlayerSwitcher( 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 { - playerEventListener?.let { currentPlayer.removeListener(it) } - setupPlayerListener() + 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) { - 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!!) + 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) } - setupPlayerListener() } private data class PlayerState( @@ -75,20 +94,30 @@ class PlayerSwitcher( } private fun stopAndClearCurrentPlayer() { - currentPlayer.stop() - if (currentPlayer is CastPlayer) { - currentPlayer.clearMediaItems() + 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) { - currentPlayer.setMediaItems( - state.mediaItems, - state.currentItemIndex, - state.playbackPositionMs - ) - currentPlayer.playWhenReady = state.playWhenReady - currentPlayer.prepare() + 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() { diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/player.dart index c2480b94..0bc85062 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/player.dart @@ -387,7 +387,9 @@ class Player { void setIndexAndUpdateIsar(int index) { _queue.setIndex = index; - _queue.updateIsarIndex(currentMedia!.id, currentIndex); + if (currentMedia != null) { + _queue.updateIsarIndex(currentMedia!.id, currentIndex); + } } List> getPositionsList() { diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue.dart index d23bd051..bbb9aac7 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue.dart @@ -10,51 +10,40 @@ import 'package:smplayer/src/shuffler.dart'; import 'package:smplayer/src/simple_shuffle.dart'; class Queue { - Queue({ - Shuffler? shuffler, - this.initializeIsar = false, - this.onInitialize, - this.beforeInitialize, - }) : _shuffler = shuffler ?? SimpleShuffler() { - IsarService.instance.isarEnabled = initializeIsar; - itemsReady = !initializeIsar; - _initialize(); - } + // ================ Constructor Parameters ================ + final Shuffler _shuffler; + final bool initializeIsar; + final Future Function(List)? onInitialize; + final Future Function()? beforeInitialize; - Future _initialize() async { - if (!itemsReady) { - try { - await beforeInitialize?.call(); + // ================ Queue State ================ + var _index = 0; + Media? _current; + bool itemsReady = false; + List> playerQueue = >[]; - final results = await Future.wait([ - previousItems, - previousPlaylistIndex, - _previousPlaylistPosition, - ]); + // ================ Previous State ================ + int previousIndex = 0; + PreviousPlaylistPosition? previousPosition; + PreviousPlaylistMusics? previousPlaylistMusics; + DateTime? _lastPrevious; - final items = results[0] as List; - previousIndex = results[1] as int; - previousPosition = results[2] as PreviousPlaylistPosition?; + // ================ Getters and Setters ================ + int get index => _index; - 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; - } - } - } + Media? get current => + _current ?? + (index >= 0 && index < playerQueue.length + ? playerQueue[index].item + : null); - var _index = 0; - int get index => _index; - Media? _current; + 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) { @@ -67,27 +56,7 @@ class Queue { _current = playerQueue[index].item; } - final Shuffler _shuffler; - final bool initializeIsar; - final Future Function(List)? onInitialize; - final Future Function()? beforeInitialize; - bool itemsReady = false; - int previousIndex = 0; - PreviousPlaylistPosition? previousPosition; - List> playerQueue = >[]; - PreviousPlaylistMusics? previousPlaylistMusics; - DateTime? _lastPrevious; - Media? get current => - _current ?? - (index >= 0 && index < playerQueue.length - ? playerQueue[index].item - : null); - - List get items { - return playerQueue.length > 0 - ? List.unmodifiable((playerQueue.map((i) => i.item).toList())) - : []; - } + // ================ ISAR ================ Future> get previousItems async { previousPlaylistMusics = await IsarService.instance @@ -109,13 +78,46 @@ class Queue { return previousPlaylistCurrentIndex?.currentIndex ?? 0; } - int get size => playerQueue.length; + 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(); - Media? get top { - if (this.size > 0) { - return playerQueue[0].item; + 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; + } } - return null; } List> _toQueueItems(List items, int i) { @@ -236,7 +238,7 @@ class Queue { } shuffle() { - if (playerQueue.length > 2) { + if (playerQueue.length > 1) { var current = playerQueue[index]; _shuffler.shuffle(playerQueue); for (var i = 0; i < playerQueue.length; ++i) { @@ -249,7 +251,7 @@ class Queue { } unshuffle() { - if (playerQueue.length > 2) { + if (playerQueue.length > 1) { var current = playerQueue[index]; _shuffler.unshuffle(playerQueue); for (var i = 0; i < playerQueue.length; ++i) { From b75ae05d51eb0c1ff8d4e6c54c808729e9b31718 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 29 May 2025 13:50:07 -0300 Subject: [PATCH 22/34] improve folders --- packages/player/lib/player.dart | 20 +++++++++---------- .../player/lib/src/{ => core}/player.dart | 20 +++++++++---------- .../lib/src/{ => core}/player_channel.dart | 18 ++++++++--------- .../{ => core}/player_event_controller.dart | 5 +++-- .../lib/src/{ => core}/player_state.dart | 0 .../src/{ => events}/before_play_event.dart | 6 +++--- .../{ => events}/current_queue_updated.dart | 4 ++-- .../{ => events}/duration_change_event.dart | 6 +++--- .../player/lib/src/{ => events}/event.dart | 4 +++- .../lib/src/{ => events}/event_type.dart | 0 .../{ => events}/network_change_event.dart | 6 +++--- .../{ => events}/position_change_event.dart | 6 +++--- .../player/lib/src/{ => models}/media.dart | 0 .../{ => models}/previous_playlist_model.dart | 0 .../previous_playlist_model.g.dart | 0 .../player/lib/src/{ => queue}/queue.dart | 14 ++++++------- .../lib/src/{ => queue}/queue_item.dart | 0 .../lib/src/{ => queue}/repeat_mode.dart | 0 .../player/lib/src/{ => queue}/shuffler.dart | 5 ++--- .../lib/src/{ => queue}/simple_shuffle.dart | 6 +++--- .../lib/src/{ => services}/isar_service.dart | 2 +- 21 files changed, 62 insertions(+), 60 deletions(-) rename packages/player/lib/src/{ => core}/player.dart (96%) rename packages/player/lib/src/{ => core}/player_channel.dart (95%) rename packages/player/lib/src/{ => core}/player_event_controller.dart (92%) rename packages/player/lib/src/{ => core}/player_state.dart (100%) rename packages/player/lib/src/{ => events}/before_play_event.dart (74%) rename packages/player/lib/src/{ => events}/current_queue_updated.dart (66%) rename packages/player/lib/src/{ => events}/duration_change_event.dart (72%) rename packages/player/lib/src/{ => events}/event.dart (89%) rename packages/player/lib/src/{ => events}/event_type.dart (100%) rename packages/player/lib/src/{ => events}/network_change_event.dart (87%) rename packages/player/lib/src/{ => events}/position_change_event.dart (75%) rename packages/player/lib/src/{ => models}/media.dart (100%) rename packages/player/lib/src/{ => models}/previous_playlist_model.dart (100%) rename packages/player/lib/src/{ => models}/previous_playlist_model.g.dart (100%) rename packages/player/lib/src/{ => queue}/queue.dart (96%) rename packages/player/lib/src/{ => queue}/queue_item.dart (100%) rename packages/player/lib/src/{ => queue}/repeat_mode.dart (100%) rename packages/player/lib/src/{ => queue}/shuffler.dart (60%) rename packages/player/lib/src/{ => queue}/simple_shuffle.dart (71%) rename packages/player/lib/src/{ => services}/isar_service.dart (98%) diff --git a/packages/player/lib/player.dart b/packages/player/lib/player.dart index 6491426f..b9c35230 100644 --- a/packages/player/lib/player.dart +++ b/packages/player/lib/player.dart @@ -1,10 +1,10 @@ -export 'src/media.dart'; -export 'src/player_state.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/core/player_state.dart'; +export 'src/core/player.dart'; +export 'src/events/event_type.dart'; +export 'src/events/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/queue/repeat_mode.dart'; diff --git a/packages/player/lib/src/player.dart b/packages/player/lib/src/core/player.dart similarity index 96% rename from packages/player/lib/src/player.dart rename to packages/player/lib/src/core/player.dart index 0bc85062..fa039d50 100644 --- a/packages/player/lib/src/player.dart +++ b/packages/player/lib/src/core/player.dart @@ -2,16 +2,16 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:smaws/aws.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/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:smplayer/src/player_channel.dart'; -import 'package:smplayer/src/player_event_controller.dart'; +import 'package:smplayer/src/core/player_channel.dart'; +import 'package:smplayer/src/core/player_event_controller.dart'; +import 'package:smplayer/src/events/event.dart'; +import 'package:smplayer/src/events/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/queue/repeat_mode.dart'; +import 'package:smplayer/src/services/isar_service.dart'; import 'player_state.dart'; diff --git a/packages/player/lib/src/player_channel.dart b/packages/player/lib/src/core/player_channel.dart similarity index 95% rename from packages/player/lib/src/player_channel.dart rename to packages/player/lib/src/core/player_channel.dart index 3cc3f6a0..2bac97fa 100644 --- a/packages/player/lib/src/player_channel.dart +++ b/packages/player/lib/src/core/player_channel.dart @@ -1,14 +1,14 @@ import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; -import 'package:smplayer/src/event.dart'; -import 'package:smplayer/src/event_type.dart'; -import 'package:smplayer/src/player.dart'; -import 'package:smplayer/src/player_state.dart'; -import 'package:smplayer/src/repeat_mode.dart'; -import 'package:smplayer/src/duration_change_event.dart'; -import 'package:smplayer/src/position_change_event.dart'; -import 'package:smplayer/src/isar_service.dart'; -import 'package:smplayer/src/previous_playlist_model.dart'; +import 'package:smplayer/src/events/event.dart'; +import 'package:smplayer/src/events/event_type.dart'; +import 'package:smplayer/src/core/player.dart'; +import 'package:smplayer/src/core/player_state.dart'; +import 'package:smplayer/src/queue/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 { diff --git a/packages/player/lib/src/player_event_controller.dart b/packages/player/lib/src/core/player_event_controller.dart similarity index 92% rename from packages/player/lib/src/player_event_controller.dart rename to packages/player/lib/src/core/player_event_controller.dart index 757512c7..f5769f60 100644 --- a/packages/player/lib/src/player_event_controller.dart +++ b/packages/player/lib/src/core/player_event_controller.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:smplayer/src/event.dart'; -import 'package:smplayer/src/event_type.dart'; + +import 'package:smplayer/src/events/event.dart'; +import 'package:smplayer/src/events/event_type.dart'; class PlayerEventController { final StreamController _eventStreamController = diff --git a/packages/player/lib/src/player_state.dart b/packages/player/lib/src/core/player_state.dart similarity index 100% rename from packages/player/lib/src/player_state.dart rename to packages/player/lib/src/core/player_state.dart diff --git a/packages/player/lib/src/before_play_event.dart b/packages/player/lib/src/events/before_play_event.dart similarity index 74% rename from packages/player/lib/src/before_play_event.dart rename to packages/player/lib/src/events/before_play_event.dart index 96beb44c..9dd67e05 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/events/event.dart'; +import 'package:smplayer/src/events/event_type.dart'; +import 'package:smplayer/src/models/media.dart'; class BeforePlayEvent extends Event { Function(bool) operation; 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..57c3adb1 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/events/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 72% rename from packages/player/lib/src/duration_change_event.dart rename to packages/player/lib/src/events/duration_change_event.dart index 5b9a0cbe..9566351a 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/events/event.dart'; +import 'package:smplayer/src/events/event_type.dart'; +import 'package:smplayer/src/models/media.dart'; class DurationChangeEvent extends Event { final Duration duration; diff --git a/packages/player/lib/src/event.dart b/packages/player/lib/src/events/event.dart similarity index 89% rename from packages/player/lib/src/event.dart rename to packages/player/lib/src/events/event.dart index 006854a8..f1dd1353 100644 --- a/packages/player/lib/src/event.dart +++ b/packages/player/lib/src/events/event.dart @@ -1,4 +1,6 @@ -import 'package:smplayer/player.dart'; +import 'package:smplayer/src/events/event_type.dart'; +import 'package:smplayer/src/models/media.dart'; +import 'package:smplayer/src/queue/repeat_mode.dart'; class Event { Event({ diff --git a/packages/player/lib/src/event_type.dart b/packages/player/lib/src/events/event_type.dart similarity index 100% rename from packages/player/lib/src/event_type.dart rename to packages/player/lib/src/events/event_type.dart diff --git a/packages/player/lib/src/network_change_event.dart b/packages/player/lib/src/events/network_change_event.dart similarity index 87% rename from packages/player/lib/src/network_change_event.dart rename to packages/player/lib/src/events/network_change_event.dart index 1195763f..c547e7b4 100644 --- a/packages/player/lib/src/network_change_event.dart +++ b/packages/player/lib/src/events/network_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/events/event.dart'; +import 'package:smplayer/src/events/event_type.dart'; +import 'package:smplayer/src/models/media.dart'; enum NetworkStatus { CONNECTED, DISCONNECTED } diff --git a/packages/player/lib/src/position_change_event.dart b/packages/player/lib/src/events/position_change_event.dart similarity index 75% rename from packages/player/lib/src/position_change_event.dart rename to packages/player/lib/src/events/position_change_event.dart index a3e6776e..c497214a 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/events/event.dart'; +import 'package:smplayer/src/events/event_type.dart'; +import 'package:smplayer/src/models/media.dart'; class PositionChangeEvent extends Event { final Duration position; diff --git a/packages/player/lib/src/media.dart b/packages/player/lib/src/models/media.dart similarity index 100% rename from packages/player/lib/src/media.dart rename to packages/player/lib/src/models/media.dart diff --git a/packages/player/lib/src/previous_playlist_model.dart b/packages/player/lib/src/models/previous_playlist_model.dart similarity index 100% rename from packages/player/lib/src/previous_playlist_model.dart rename to packages/player/lib/src/models/previous_playlist_model.dart diff --git a/packages/player/lib/src/previous_playlist_model.g.dart b/packages/player/lib/src/models/previous_playlist_model.g.dart similarity index 100% rename from packages/player/lib/src/previous_playlist_model.g.dart rename to packages/player/lib/src/models/previous_playlist_model.g.dart diff --git a/packages/player/lib/src/queue.dart b/packages/player/lib/src/queue/queue.dart similarity index 96% rename from packages/player/lib/src/queue.dart rename to packages/player/lib/src/queue/queue.dart index bbb9aac7..e28e169c 100644 --- a/packages/player/lib/src/queue.dart +++ b/packages/player/lib/src/queue/queue.dart @@ -1,13 +1,13 @@ 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'; +import 'package:smplayer/src/models/media.dart'; +import 'package:smplayer/src/models/previous_playlist_model.dart'; +import 'package:smplayer/src/queue/queue_item.dart'; +import 'package:smplayer/src/queue/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 ================ diff --git a/packages/player/lib/src/queue_item.dart b/packages/player/lib/src/queue/queue_item.dart similarity index 100% rename from packages/player/lib/src/queue_item.dart rename to packages/player/lib/src/queue/queue_item.dart diff --git a/packages/player/lib/src/repeat_mode.dart b/packages/player/lib/src/queue/repeat_mode.dart similarity index 100% rename from packages/player/lib/src/repeat_mode.dart rename to packages/player/lib/src/queue/repeat_mode.dart 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..31a92500 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/queue/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..2bd0a597 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/queue/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/isar_service.dart b/packages/player/lib/src/services/isar_service.dart similarity index 98% rename from packages/player/lib/src/isar_service.dart rename to packages/player/lib/src/services/isar_service.dart index cb6ecb88..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._(); From e7ac240b8ab027af3687aa518bfffa83de7002b0 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 29 May 2025 14:04:18 -0300 Subject: [PATCH 23/34] Refactor event and player state management by moving enums to a dedicated directory, removing unused files, and updating imports accordingly. --- packages/player/lib/player.dart | 8 ++++---- packages/player/lib/src/core/player.dart | 8 ++++---- packages/player/lib/src/core/player_channel.dart | 8 ++++---- packages/player/lib/src/core/player_event_controller.dart | 4 ++-- packages/player/lib/src/{events => enums}/event_type.dart | 0 packages/player/lib/src/{core => enums}/player_state.dart | 0 packages/player/lib/src/{queue => enums}/repeat_mode.dart | 0 packages/player/lib/src/events/before_play_event.dart | 4 ++-- packages/player/lib/src/events/current_queue_updated.dart | 2 +- packages/player/lib/src/events/duration_change_event.dart | 4 ++-- packages/player/lib/src/events/network_change_event.dart | 4 ++-- packages/player/lib/src/events/position_change_event.dart | 4 ++-- packages/player/lib/src/{events => models}/event.dart | 4 ++-- packages/player/lib/src/{queue => models}/queue_item.dart | 0 packages/player/lib/src/queue/queue.dart | 4 ++-- packages/player/lib/src/queue/shuffler.dart | 2 +- packages/player/lib/src/queue/simple_shuffle.dart | 2 +- 17 files changed, 29 insertions(+), 29 deletions(-) rename packages/player/lib/src/{events => enums}/event_type.dart (100%) rename packages/player/lib/src/{core => enums}/player_state.dart (100%) rename packages/player/lib/src/{queue => enums}/repeat_mode.dart (100%) rename packages/player/lib/src/{events => models}/event.dart (92%) rename packages/player/lib/src/{queue => models}/queue_item.dart (100%) diff --git a/packages/player/lib/player.dart b/packages/player/lib/player.dart index b9c35230..137a487b 100644 --- a/packages/player/lib/player.dart +++ b/packages/player/lib/player.dart @@ -1,10 +1,10 @@ export 'src/models/media.dart'; -export 'src/core/player_state.dart'; +export 'src/enums/player_state.dart'; export 'src/core/player.dart'; -export 'src/events/event_type.dart'; -export 'src/events/event.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/queue/repeat_mode.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 index fa039d50..3bae5676 100644 --- a/packages/player/lib/src/core/player.dart +++ b/packages/player/lib/src/core/player.dart @@ -4,16 +4,16 @@ 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/events/event.dart'; -import 'package:smplayer/src/events/event_type.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/queue/repeat_mode.dart'; +import 'package:smplayer/src/enums/repeat_mode.dart'; import 'package:smplayer/src/services/isar_service.dart'; -import 'player_state.dart'; +import '../enums/player_state.dart'; class Player { Player({ diff --git a/packages/player/lib/src/core/player_channel.dart b/packages/player/lib/src/core/player_channel.dart index 2bac97fa..0ce591c3 100644 --- a/packages/player/lib/src/core/player_channel.dart +++ b/packages/player/lib/src/core/player_channel.dart @@ -1,10 +1,10 @@ import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; -import 'package:smplayer/src/events/event.dart'; -import 'package:smplayer/src/events/event_type.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/core/player_state.dart'; -import 'package:smplayer/src/queue/repeat_mode.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'; diff --git a/packages/player/lib/src/core/player_event_controller.dart b/packages/player/lib/src/core/player_event_controller.dart index f5769f60..e2ad690d 100644 --- a/packages/player/lib/src/core/player_event_controller.dart +++ b/packages/player/lib/src/core/player_event_controller.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:smplayer/src/events/event.dart'; -import 'package:smplayer/src/events/event_type.dart'; +import 'package:smplayer/src/models/event.dart'; +import 'package:smplayer/src/enums/event_type.dart'; class PlayerEventController { final StreamController _eventStreamController = diff --git a/packages/player/lib/src/events/event_type.dart b/packages/player/lib/src/enums/event_type.dart similarity index 100% rename from packages/player/lib/src/events/event_type.dart rename to packages/player/lib/src/enums/event_type.dart diff --git a/packages/player/lib/src/core/player_state.dart b/packages/player/lib/src/enums/player_state.dart similarity index 100% rename from packages/player/lib/src/core/player_state.dart rename to packages/player/lib/src/enums/player_state.dart diff --git a/packages/player/lib/src/queue/repeat_mode.dart b/packages/player/lib/src/enums/repeat_mode.dart similarity index 100% rename from packages/player/lib/src/queue/repeat_mode.dart rename to packages/player/lib/src/enums/repeat_mode.dart diff --git a/packages/player/lib/src/events/before_play_event.dart b/packages/player/lib/src/events/before_play_event.dart index 9dd67e05..2c366f7e 100644 --- a/packages/player/lib/src/events/before_play_event.dart +++ b/packages/player/lib/src/events/before_play_event.dart @@ -1,5 +1,5 @@ -import 'package:smplayer/src/events/event.dart'; -import 'package:smplayer/src/events/event_type.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 { diff --git a/packages/player/lib/src/events/current_queue_updated.dart b/packages/player/lib/src/events/current_queue_updated.dart index 57c3adb1..18c99503 100644 --- a/packages/player/lib/src/events/current_queue_updated.dart +++ b/packages/player/lib/src/events/current_queue_updated.dart @@ -1,4 +1,4 @@ -import 'package:smplayer/src/events/event.dart'; +import 'package:smplayer/src/models/event.dart'; import 'package:smplayer/src/models/media.dart'; class CurrentQueueUpdated extends Event { diff --git a/packages/player/lib/src/events/duration_change_event.dart b/packages/player/lib/src/events/duration_change_event.dart index 9566351a..3bed8fdb 100644 --- a/packages/player/lib/src/events/duration_change_event.dart +++ b/packages/player/lib/src/events/duration_change_event.dart @@ -1,5 +1,5 @@ -import 'package:smplayer/src/events/event.dart'; -import 'package:smplayer/src/events/event_type.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 { diff --git a/packages/player/lib/src/events/network_change_event.dart b/packages/player/lib/src/events/network_change_event.dart index c547e7b4..d83c66ac 100644 --- a/packages/player/lib/src/events/network_change_event.dart +++ b/packages/player/lib/src/events/network_change_event.dart @@ -1,5 +1,5 @@ -import 'package:smplayer/src/events/event.dart'; -import 'package:smplayer/src/events/event_type.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 } diff --git a/packages/player/lib/src/events/position_change_event.dart b/packages/player/lib/src/events/position_change_event.dart index c497214a..11f87aed 100644 --- a/packages/player/lib/src/events/position_change_event.dart +++ b/packages/player/lib/src/events/position_change_event.dart @@ -1,5 +1,5 @@ -import 'package:smplayer/src/events/event.dart'; -import 'package:smplayer/src/events/event_type.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 { diff --git a/packages/player/lib/src/events/event.dart b/packages/player/lib/src/models/event.dart similarity index 92% rename from packages/player/lib/src/events/event.dart rename to packages/player/lib/src/models/event.dart index f1dd1353..2b4797e9 100644 --- a/packages/player/lib/src/events/event.dart +++ b/packages/player/lib/src/models/event.dart @@ -1,6 +1,6 @@ -import 'package:smplayer/src/events/event_type.dart'; +import 'package:smplayer/src/enums/event_type.dart'; import 'package:smplayer/src/models/media.dart'; -import 'package:smplayer/src/queue/repeat_mode.dart'; +import 'package:smplayer/src/enums/repeat_mode.dart'; class Event { Event({ diff --git a/packages/player/lib/src/queue/queue_item.dart b/packages/player/lib/src/models/queue_item.dart similarity index 100% rename from packages/player/lib/src/queue/queue_item.dart rename to packages/player/lib/src/models/queue_item.dart diff --git a/packages/player/lib/src/queue/queue.dart b/packages/player/lib/src/queue/queue.dart index e28e169c..32a0ea3b 100644 --- a/packages/player/lib/src/queue/queue.dart +++ b/packages/player/lib/src/queue/queue.dart @@ -3,8 +3,8 @@ 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/queue/queue_item.dart'; -import 'package:smplayer/src/queue/repeat_mode.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'; diff --git a/packages/player/lib/src/queue/shuffler.dart b/packages/player/lib/src/queue/shuffler.dart index 31a92500..7ef532ca 100644 --- a/packages/player/lib/src/queue/shuffler.dart +++ b/packages/player/lib/src/queue/shuffler.dart @@ -1,5 +1,5 @@ import 'package:smplayer/src/models/media.dart'; -import 'package:smplayer/src/queue/queue_item.dart'; +import 'package:smplayer/src/models/queue_item.dart'; abstract class Shuffler { List> shuffle(List> list); diff --git a/packages/player/lib/src/queue/simple_shuffle.dart b/packages/player/lib/src/queue/simple_shuffle.dart index 2bd0a597..a9ca3402 100644 --- a/packages/player/lib/src/queue/simple_shuffle.dart +++ b/packages/player/lib/src/queue/simple_shuffle.dart @@ -1,5 +1,5 @@ import 'package:smplayer/src/models/media.dart'; -import 'package:smplayer/src/queue/queue_item.dart'; +import 'package:smplayer/src/models/queue_item.dart'; import 'package:smplayer/src/queue/shuffler.dart'; class SimpleShuffler extends Shuffler { From 130505d7b8e235e6d67444f3e90d8e7ad746a38c Mon Sep 17 00:00:00 2001 From: lstonussi Date: Thu, 29 May 2025 15:36:55 -0300 Subject: [PATCH 24/34] Refactor MediaService by encapsulating player variables and ensuring proper resource release on destruction. --- .../kotlin/br/com/suamusica/player/MediaService.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt b/packages/player/android/src/main/kotlin/br/com/suamusica/player/MediaService.kt index 0fee0224..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 @@ -14,7 +14,6 @@ import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.Player.REPEAT_MODE_ALL import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi @@ -62,21 +61,16 @@ 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() - var playerSwitcher: PlayerSwitcher? = null - var exoPlayer: ExoPlayer? = null - - var castPlayer: CastPlayer? = 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 @@ -267,6 +261,8 @@ class MediaService : MediaSessionService() { } override fun onDestroy() { + exoPlayer?.release() + exoPlayer = null mediaSession.run { releaseAndPerformAndDisableTracking() player.release() From 6920f13dbfe3f798a5084b13e2b32a262138e7bf Mon Sep 17 00:00:00 2001 From: lstonussi Date: Sat, 31 May 2025 13:47:23 -0300 Subject: [PATCH 25/34] fix channel --- .../player/lib/src/core/player_channel.dart | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/player/lib/src/core/player_channel.dart b/packages/player/lib/src/core/player_channel.dart index 0ce591c3..4ccf5012 100644 --- a/packages/player/lib/src/core/player_channel.dart +++ b/packages/player/lib/src/core/player_channel.dart @@ -27,10 +27,28 @@ class PlayerChannel { String method, [ Map? arguments, ]) async { - arguments ??= const {}; - return _channel - .invokeMethod(method, arguments) - .then((result) => result ?? Future.value(ok)); + 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 { From 30d2268b486aa3553dcdcdd474dbadef17b7892e Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 2 Jun 2025 01:18:54 -0300 Subject: [PATCH 26/34] Add Logger, NotificationManager, NowPlayingInfoManager, and QueueManager classes to enhance media playback functionality and improve player state management --- packages/player/ios/Classes/Logger.swift | 9 + .../ios/Classes/NotificationManager.swift | 40 +++ .../ios/Classes/NowPlayingInfoManager.swift | 69 ++++ .../player/ios/Classes/QueueManager.swift | 200 +++++++++++ packages/player/ios/Classes/SMPlayer.swift | 324 +++++++----------- .../ios/Classes/SMPlayerListeners.swift | 2 +- 6 files changed, 439 insertions(+), 205 deletions(-) create mode 100644 packages/player/ios/Classes/Logger.swift create mode 100644 packages/player/ios/Classes/NotificationManager.swift create mode 100644 packages/player/ios/Classes/NowPlayingInfoManager.swift create mode 100644 packages/player/ios/Classes/QueueManager.swift diff --git a/packages/player/ios/Classes/Logger.swift b/packages/player/ios/Classes/Logger.swift new file mode 100644 index 00000000..859dc1db --- /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 + } +} \ No newline at end of file diff --git a/packages/player/ios/Classes/NotificationManager.swift b/packages/player/ios/Classes/NotificationManager.swift new file mode 100644 index 00000000..e4902b80 --- /dev/null +++ b/packages/player/ios/Classes/NotificationManager.swift @@ -0,0 +1,40 @@ +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) + } +} \ 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..48b66297 --- /dev/null +++ b/packages/player/ios/Classes/NowPlayingInfoManager.swift @@ -0,0 +1,69 @@ +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.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.changePlaybackPositionCommand.isEnabled = true + } + + func removeNotification() { + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.nextTrackCommand.isEnabled = false + commandCenter.previousTrackCommand.isEnabled = false + commandCenter.changePlaybackPositionCommand.isEnabled = false + commandCenter.playCommand.removeTarget(nil) + commandCenter.pauseCommand.removeTarget(nil) + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + try? AVAudioSession.sharedInstance().setActive(false) + UIApplication.shared.endReceivingRemoteControlEvents() + } + + func clearNowPlayingInfo() { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + removeNotification() + } +} \ No newline at end of file diff --git a/packages/player/ios/Classes/QueueManager.swift b/packages/player/ios/Classes/QueueManager.swift new file mode 100644 index 00000000..3b0488c3 --- /dev/null +++ b/packages/player/ios/Classes/QueueManager.swift @@ -0,0 +1,200 @@ +import AVFoundation + +class QueueManager { + var historyQueue: [AVPlayerItem] = [] + var futureQueue: [AVPlayerItem] = [] + var originalQueue: [AVPlayerItem] = [] + var shuffledQueue: [AVPlayerItem] = [] + var shuffledIndices: [Int] = [] + var isShuffleModeEnabled: Bool = false + private let smPlayer: AVQueuePlayer + private let maxTotalItems: Int + + init(smPlayer: AVQueuePlayer, maxTotalItems: Int = 5) { + self.smPlayer = smPlayer + self.maxTotalItems = maxTotalItems + } + + var fullQueue: [AVPlayerItem] { + return historyQueue + smPlayer.items() + futureQueue + } + + var currentIndex: Int { + guard let currentItem = smPlayer.currentItem else { + return 0 + } + return fullQueue.firstIndex(of: currentItem) ?? 0 + } + + func fillShuffledQueue() { + shuffledQueue.removeAll() + for index in shuffledIndices { + if index < fullQueue.count { + shuffledQueue.append(fullQueue[index]) + } + } + } + + 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]) { + 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) + } + } + + 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: [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() + if position >= 0 && position < currentQueue.count { + 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 insertIntoPlayerIfNeeded() { + let itemsToAdd = min(maxTotalItems - smPlayer.items().count, futureQueue.count) + for _ in 0.. AVPlayerItem?, cookie: String?) { + var fullQueueUpdated = fullQueue + if let index = fullQueue.firstIndex(where: { $0.playlistItem?.mediaId == id }) { + let oldItem = fullQueueUpdated[index] + if let playerItem = createPlayerItemFromUri(uri, oldItem.playlistItem?.fallbackUrl, cookie) { + playerItem.playlistItem = oldItem.playlistItem + fullQueueUpdated[index] = playerItem + distributeItemsInRightQueue(currentQueue: fullQueueUpdated) + } + } + } + + func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false, seekToPosition: ((Int) -> Void)? = nil, pause: (() -> Void)? = nil, play: (() -> Void)? = nil, notifyCurrentMediaIndex: ((Int) -> Void)? = nil, addMediaChangeObserver: (() -> Void)? = nil, shouldNotifyTransition: inout Bool) { + distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position, completionHandler: { + notifyCurrentMediaIndex?(self.currentIndex) + if timePosition > 0 { + seekToPosition?(timePosition) + } + }) + if loadOnly { + pause?() + shouldNotifyTransition = false + } else { + play?() + } + addMediaChangeObserver?() + } + + func nextTrack(from: String, playFromQueue: ((Int) -> Void)? = nil, play: (() -> Void)? = nil) { + smPlayer.pause() + if let currentItem = smPlayer.currentItem { + historyQueue.append(currentItem) + } + if smPlayer.currentItem == fullQueue.last && smPlayer.repeatMode == .REPEAT_MODE_ALL { + playFromQueue?(0) + } + smPlayer.advanceToNextItem() + smPlayer.seek(to: CMTime.zero) + insertIntoPlayerIfNeeded() + play?() + } + + func previousTrack(seekToPosition: ((Int) -> Void)? = nil, play: (() -> Void)? = nil) { + smPlayer.pause() + guard let lastHistoryItem = historyQueue.popLast() else { + seekToPosition?(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) + smPlayer.seek(to: CMTime.zero) + insertIntoPlayerIfNeeded() + play?() + } + + func getCurrentPlaylistItem() -> PlaylistItem? { + guard let currentItem = smPlayer.currentItem else { + return nil + } + return currentItem.playlistItem + } + + func printStatus(from: String) { + // Implementação opcional para debug + } + + func enqueue(item: AVPlayerItem) { + futureQueue.append(item) + } +} \ No newline at end of file diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index adf5f423..c5ce1178 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 @@ -9,54 +8,58 @@ public class SMPlayer : NSObject { 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] = [] + private var queueManager: QueueManager private var listeners: SMPlayerListeners? = nil // Transition Control private var shouldNotifyTransition: Bool = true var areNotificationCommandsEnabled: Bool = true + private var notificationManager: NotificationManager! + private var nowPlayingInfoManager: NowPlayingInfoManager! + + private enum Constants { + static let maxTotalItems = 5 + static let defaultTimescale: CMTimeScale = 60000 + } + var fullQueue: [AVPlayerItem] { - return historyQueue + smPlayer.items() + futureQueue + 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?) { smPlayer = AVQueuePlayer() + queueManager = QueueManager(smPlayer: smPlayer, maxTotalItems: Constants.maxTotalItems) + super.init() + nowPlayingInfoManager = NowPlayingInfoManager() + notificationManager = NotificationManager(target: self) self.methodChannelManager = methodChannelManager listeners = SMPlayerListeners(smPlayer:smPlayer,methodChannelManager:methodChannelManager) - - NotificationCenter.default.addObserver( - self, - selector: #selector(handleInterruption(_:)), - name: AVAudioSession.interruptionNotification, - object: nil - ) - - listeners?.onMediaChanged = { [self] in + notificationManager.addAudioInterruptionObserver(selector: #selector(handleInterruption(_:))) + listeners?.onMediaChanged = { [weak self] in + guard let self = self else { return } if(self.smPlayer.items().count > 0){ - if(self.smPlayer.currentItem != self.fullQueue.first && self.historyQueue.count > 0 && shouldNotifyTransition){ + if(self.smPlayer.currentItem != self.fullQueue.first && self.queueManager.historyQueue.count > 0 && shouldNotifyTransition){ methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) } shouldNotifyTransition = true self.updateEndPlaybackObserver() self.listeners?.addItemsObservers() - methodChannelManager?.currentMediaIndex(index: self.currentIndex) + methodChannelManager?.currentMediaIndex(index: self.currentIndex) } } - setupNowPlayingInfoCenter() + nowPlayingInfoManager.setupNowPlayingInfoCenter( + areNotificationCommandsEnabled: { [weak self] in self?.areNotificationCommandsEnabled ?? true }, + play: { [weak self] in self?.play() }, + pause: { [weak self] in self?.pause() }, + nextTrack: { [weak self] in self?.nextTrack(from: "commandCenter.nextTrackCommand") }, + previousTrack: { [weak self] in self?.previousTrack() }, + seekToPosition: { [weak self] pos in self?.seekToPosition(position: pos) } + ) _ = AudioSessionManager.activeSession() } @@ -87,21 +90,12 @@ public class SMPlayer : NSObject { 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) } } @@ -137,8 +131,25 @@ public class SMPlayer : NSObject { } func clearNowPlayingInfo() { - MPNowPlayingInfoCenter.default().nowPlayingInfo = nil - removeNotification() + nowPlayingInfoManager.clearNowPlayingInfo() + } + + private func createPlayerItem(with url: URL, cookie: String?) -> AVPlayerItem { + let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie ?? ""]] + return AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) + } + + private func createLocalPlayerItem(with path: String) -> AVPlayerItem { + return AVPlayerItem(asset: AVAsset(url: NSURL(fileURLWithPath: path) as URL)) + } + + private func createPlayerItemFromUri(_ uri: String?, fallbackUrl: String?, cookie: String?) -> AVPlayerItem? { + if uri?.contains("https") ?? true { + guard let url = URL(string: (uri ?? fallbackUrl!) ?? "") else { return nil } + return createPlayerItem(with: url, cookie: cookie) + } else { + return createLocalPlayerItem(with: uri!) + } } func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { @@ -149,84 +160,49 @@ public class SMPlayer : NSObject { } 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 = createPlayerItemFromUri(media.url, fallbackUrl: nil, cookie: self.cookie) + media.cookie = cookie + if playerItem != nil { + playerItem!.playlistItem = media + queueManager.enqueue(item: playerItem!) } - playerItem!.playlistItem = media - futureQueue.append(playerItem!) } insertIntoPlayerIfNeeded() if autoPlay && isFirstBatch { self.smPlayer.play() self.setNowPlaying() - self.enableCommands() } self.enableCommands() + //TODO: precisa adicionar em todos os batches? listeners?.addPlayerObservers() } 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") - } + queueManager.removeByPosition(indexes: indexes) + 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]) - } - } + queueManager.toggleShuffle(positionsList: positionsList) + methodChannelManager?.shuffleChanged(shuffleIsActive: queueManager.isShuffleModeEnabled) } - 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) + queueManager.reorder(fromIndex: fromIndex, toIndex: toIndex) } func nextTrack(from:String) { smPlayer.pause() - print("#print nextTrack \(from)") + Logger.debugLog("#print nextTrack \(from)") if let currentItem = smPlayer.currentItem { - historyQueue.append(currentItem) + queueManager.historyQueue.append(currentItem) } if(smPlayer.currentItem == fullQueue.last && smPlayer.repeatMode == .REPEAT_MODE_ALL){ playFromQueue(position: 0) } smPlayer.advanceToNextItem() - seekToPosition(position: 0) + smPlayer.seek(to: CMTime.zero) insertIntoPlayerIfNeeded() smPlayer.play() printStatus(from:"NEXT") @@ -235,7 +211,7 @@ public class SMPlayer : NSObject { func previousTrack() { smPlayer.pause() - guard let lastHistoryItem = historyQueue.popLast() else { + guard let lastHistoryItem = queueManager.historyQueue.popLast() else { seekToPosition(position: 0) return } @@ -244,14 +220,14 @@ public class SMPlayer : NSObject { if(currentItem != lastItemInPlayer) { smPlayer.remove(lastItemInPlayer) - futureQueue.insert(lastItemInPlayer, at: 0) + queueManager.futureQueue.insert(lastItemInPlayer, at: 0) } smPlayer.insert(lastHistoryItem, after: currentItem) smPlayer.advanceToNextItem() smPlayer.insert(currentItem, after: smPlayer.currentItem) - seekToPosition(position: 0) + smPlayer.seek(to: CMTime.zero) insertIntoPlayerIfNeeded() smPlayer.play() printStatus(from:"previousTrack") @@ -262,17 +238,18 @@ public class SMPlayer : NSObject { } private func insertIntoPlayerIfNeeded() { - let maxTotalItems = 5 - let itemsToAdd = min(maxTotalItems - smPlayer.items().count, futureQueue.count) + let maxTotalItems = Constants.maxTotalItems + let itemsToAdd = min(maxTotalItems - smPlayer.items().count, queueManager.futureQueue.count) for _ in 0.. \(String(describing: smPlayer.items()))") smPlayer.insert(item, after: nil) - futureQueue.removeFirst() + queueManager.futureQueue.removeFirst() } } - print("#NATIVE LOGS insertIntoPlayerIfNeeded ==> \(String(describing: smPlayer.currentItem?.playlistItem?.title))") - printStatus(from:"insertIntoPlayerIfNeeded") + Logger.debugLog("#NATIVE LOGS insertIntoPlayerIfNeeded ==> \(String(describing: smPlayer.currentItem?.playlistItem?.title))") + printStatus(from:"insertIntoPlayerIfNeeded") } @@ -280,24 +257,15 @@ public class SMPlayer : NSObject { smPlayer.pause() smPlayer.seek(to: CMTime.zero) smPlayer.removeAllItems() - historyQueue.removeAll() - futureQueue.removeAll() - originalQueue.removeAll() - shuffledQueue.removeAll() - shuffledIndices.removeAll() + queueManager.historyQueue.removeAll() + queueManager.futureQueue.removeAll() + queueManager.originalQueue.removeAll() + queueManager.shuffledQueue.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() + nowPlayingInfoManager.removeNotification() } func play(){ @@ -305,7 +273,7 @@ public class SMPlayer : NSObject { } func seekToPosition(position:Int){ - let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: 60000) + let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: Constants.defaultTimescale) smPlayer.currentItem?.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { completed in if completed { self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.seekEnd) @@ -321,10 +289,11 @@ public class SMPlayer : NSObject { } private func distributeItemsInRightQueue(currentQueue: [AVPlayerItem], keepFirst: Bool = true, positionArg: Int = -1, completionHandler completion: (() -> Void)? = nil) { + let startTime = CFAbsoluteTimeGetCurrent() guard currentQueue.count > 0 else { return } var position = positionArg - historyQueue.removeAll() - futureQueue.removeAll() + queueManager.historyQueue.removeAll() + queueManager.futureQueue.removeAll() if(keepFirst){ position = smPlayer.currentItem != nil ? currentQueue.firstIndex(of:smPlayer.currentItem!) ?? -1 : -1 @@ -334,43 +303,39 @@ public class SMPlayer : NSObject { } }else{ smPlayer.removeAllItems() - futureQueue.append(currentQueue[position]) + queueManager.futureQueue.append(currentQueue[position]) } for (index, item) in currentQueue.enumerated() { if(index != position){ if index < position { - historyQueue.append(item) + queueManager.historyQueue.append(item) } else { - futureQueue.append(item) + queueManager.futureQueue.append(item) } } } insertIntoPlayerIfNeeded() completion?() + + let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime + print("TIME distributeItemsInRightQueue execution time: \(timeElapsed) seconds") } 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)") + if let playerItem = createPlayerItemFromUri(uri, fallbackUrl: oldItem.playlistItem?.fallbackUrl, cookie: oldItem.playlistItem?.cookie) { + 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) } - distributeItemsInRightQueue(currentQueue: fullQueueUpdated) } - } func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false) { @@ -391,90 +356,36 @@ public class SMPlayer : NSObject { } 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") + Logger.debugLog("QueueActivity #################################################") + Logger.debugLog("QueueActivity \(from) ") + Logger.debugLog("QueueActivity Current Index: \(String(describing: currentIndex))") + Logger.debugLog("QueueActivity ------------------------------------------") + Logger.debugLog("QueueActivity printStatus History: \(queueManager.historyQueue.count) items") - for item in historyQueue { - print("QueueActivity printStatus History: \(String(describing: item.playlistItem?.title))") + for item in queueManager.historyQueue { + Logger.debugLog("QueueActivity printStatus History: \(String(describing: item.playlistItem?.title))") } - print("QueueActivity printStatus ------------------------------------------") - print("QueueActivity printStatus futureQueue Items: \(futureQueue.count) items") + Logger.debugLog("QueueActivity printStatus ------------------------------------------") + Logger.debugLog("QueueActivity printStatus futureQueue Items: \(queueManager.futureQueue.count) items") - for item in futureQueue { - print("QueueActivity printStatus Upcoming: \(String(describing: item.playlistItem?.title))") + for item in queueManager.futureQueue { + Logger.debugLog("QueueActivity printStatus Upcoming: \(String(describing: item.playlistItem?.title))") } - print("QueueActivity printStatus ------------------------------------------") - print("QueueActivity printStatus AVQueuePlayer items: \(smPlayer.items().count)") + Logger.debugLog("QueueActivity printStatus ------------------------------------------") + Logger.debugLog("QueueActivity printStatus AVQueuePlayer items: \(smPlayer.items().count)") for item in smPlayer.items() { - print("QueueActivity printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") + Logger.debugLog("QueueActivity printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") } - print("QueueActivity printStatus #################################################") - } + Logger.debugLog("QueueActivity printStatus #################################################") + } - func isDebugMode() -> Bool { - #if DEBUG - return true - #else - return false - #endif - } - //override automatic next @objc func itemDidFinishPlaying(_ notification: Notification) { pause() @@ -492,6 +403,11 @@ public class SMPlayer : NSObject { } play() } + + private func removeAllObservers() { + notificationManager.removeAllObservers() + // Remover outros observadores + } } extension AVPlayerItem { diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index 398968af..224753e7 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -33,7 +33,7 @@ public class SMPlayerListeners : NSObject { case .failed: if let error = playerItem.error { print("#NATIVE LOGS ==> ERROR: \(String(describing: playerItem.error))") - self.methodChannelManager?.notifyError(error: "UNKNOW ERROR") + self.methodChannelManager?.notifyError(error: "UNKNOWN ERROR") } case .readyToPlay: self.notifyPlayerStateChange(state: PlayerState.stateReady) From f7edd69d1c54bea77f49010e67db2b3ef8014bdd Mon Sep 17 00:00:00 2001 From: lstonussi Date: Mon, 2 Jun 2025 10:10:33 -0300 Subject: [PATCH 27/34] improve listeners --- .../player/ios/Classes/PlayerPlugin.swift | 13 +- .../player/ios/Classes/QueueManager.swift | 34 ++- packages/player/ios/Classes/SMPlayer.swift | 208 +++--------------- .../ios/Classes/SMPlayerListeners.swift | 135 ++++++------ 4 files changed, 137 insertions(+), 253 deletions(-) diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index 20b2dd0e..ae0b3585 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -35,7 +35,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { } result(NSNumber(value: true)) case "next": - smPlayer?.nextTrack(from:"next call") + smPlayer?.queueManager.nextTrack(from:"next call") result(NSNumber(value: 1)) case "disable_notification_commands": smPlayer?.areNotificationCommandsEnabled = false @@ -48,7 +48,7 @@ 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() @@ -66,7 +66,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": @@ -77,14 +77,13 @@ 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": diff --git a/packages/player/ios/Classes/QueueManager.swift b/packages/player/ios/Classes/QueueManager.swift index 3b0488c3..fca28108 100644 --- a/packages/player/ios/Classes/QueueManager.swift +++ b/packages/player/ios/Classes/QueueManager.swift @@ -52,6 +52,7 @@ class QueueManager { } distributeItemsInRightQueue(currentQueue: queueAfterRemovedItems, keepFirst: true) } + printStatus(from: "removeByPosition") } func toggleShuffle(positionsList: [[String: Int]]) { @@ -108,6 +109,8 @@ class QueueManager { futureQueue.removeFirst() } } + Logger.debugLog("#NATIVE LOGS insertIntoPlayerIfNeeded ==> \(String(describing: smPlayer.currentItem?.playlistItem?.title))") + printStatus(from:"insertIntoPlayerIfNeeded") } func removeAll() { @@ -133,7 +136,7 @@ class QueueManager { } } - func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false, seekToPosition: ((Int) -> Void)? = nil, pause: (() -> Void)? = nil, play: (() -> Void)? = nil, notifyCurrentMediaIndex: ((Int) -> Void)? = nil, addMediaChangeObserver: (() -> Void)? = nil, shouldNotifyTransition: inout Bool) { + func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false, seekToPosition: ((Int) -> Void)? = nil, pause: (() -> Void)? = nil, play: (() -> Void)? = nil, notifyCurrentMediaIndex: ((Int) -> Void)? = nil, addMediaChangeObserver: (() -> Void)? = nil) { distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position, completionHandler: { notifyCurrentMediaIndex?(self.currentIndex) if timePosition > 0 { @@ -142,11 +145,11 @@ class QueueManager { }) if loadOnly { pause?() - shouldNotifyTransition = false +// shouldNotifyTransition = false } else { play?() } - addMediaChangeObserver?() +// addMediaChangeObserver?() } func nextTrack(from: String, playFromQueue: ((Int) -> Void)? = nil, play: (() -> Void)? = nil) { @@ -191,10 +194,31 @@ class QueueManager { } func printStatus(from: String) { - // Implementação opcional para debug + Logger.debugLog("QueueActivity #################################################") + Logger.debugLog("QueueActivity \(from) ") + Logger.debugLog("QueueActivity Current Index: \(String(describing: currentIndex))") + Logger.debugLog("QueueActivity ------------------------------------------") + Logger.debugLog("QueueActivity printStatus History: \(historyQueue.count) items") + + for item in historyQueue { + Logger.debugLog("QueueActivity printStatus History: \(String(describing: item.playlistItem?.title))") + } + Logger.debugLog("QueueActivity printStatus ------------------------------------------") + Logger.debugLog("QueueActivity printStatus futureQueue Items: \(futureQueue.count) items") + + for item in futureQueue { + Logger.debugLog("QueueActivity printStatus Upcoming: \(String(describing: item.playlistItem?.title))") + } + Logger.debugLog("QueueActivity printStatus ------------------------------------------") + Logger.debugLog("QueueActivity printStatus AVQueuePlayer items: \(smPlayer.items().count)") + + for item in smPlayer.items() { + Logger.debugLog("QueueActivity printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") + } + Logger.debugLog("QueueActivity printStatus #################################################") } func enqueue(item: AVPlayerItem) { futureQueue.append(item) } -} \ No newline at end of file +} diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index c5ce1178..982dd009 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -8,10 +8,9 @@ public class SMPlayer : NSObject { private var cookie: String = "" //Queue handle private var smPlayer: AVQueuePlayer - private var queueManager: QueueManager + var queueManager: QueueManager private var listeners: SMPlayerListeners? = nil // Transition Control - private var shouldNotifyTransition: Bool = true var areNotificationCommandsEnabled: Bool = true private var notificationManager: NotificationManager! @@ -40,24 +39,27 @@ public class SMPlayer : NSObject { self.methodChannelManager = methodChannelManager listeners = SMPlayerListeners(smPlayer:smPlayer,methodChannelManager:methodChannelManager) notificationManager.addAudioInterruptionObserver(selector: #selector(handleInterruption(_:))) - listeners?.onMediaChanged = { [weak self] in + listeners?.onMediaChanged = { [weak self] shouldNotify in guard let self = self else { return } - if(self.smPlayer.items().count > 0){ - if(self.smPlayer.currentItem != self.fullQueue.first && self.queueManager.historyQueue.count > 0 && shouldNotifyTransition){ - methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) - } - shouldNotifyTransition = true - self.updateEndPlaybackObserver() - self.listeners?.addItemsObservers() - methodChannelManager?.currentMediaIndex(index: self.currentIndex) + 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) } nowPlayingInfoManager.setupNowPlayingInfoCenter( areNotificationCommandsEnabled: { [weak self] in self?.areNotificationCommandsEnabled ?? true }, play: { [weak self] in self?.play() }, pause: { [weak self] in self?.pause() }, - nextTrack: { [weak self] in self?.nextTrack(from: "commandCenter.nextTrackCommand") }, - previousTrack: { [weak self] in self?.previousTrack() }, + nextTrack: { [weak self] in self?.queueManager.nextTrack(from: "commandCenter.nextTrackCommand") }, + previousTrack: { [weak self] in self?.queueManager.previousTrack() }, seekToPosition: { [weak self] pos in self?.seekToPosition(position: pos) } ) _ = AudioSessionManager.activeSession() @@ -167,7 +169,7 @@ public class SMPlayer : NSObject { queueManager.enqueue(item: playerItem!) } } - insertIntoPlayerIfNeeded() + queueManager.insertIntoPlayerIfNeeded() if autoPlay && isFirstBatch { self.smPlayer.play() self.setNowPlaying() @@ -177,90 +179,18 @@ public class SMPlayer : NSObject { listeners?.addPlayerObservers() } - func removeByPosition(indexes: [Int]) { - queueManager.removeByPosition(indexes: indexes) - printStatus(from: "removeByPosition") - } - func toggleShuffle(positionsList: [[String: Int]]) { queueManager.toggleShuffle(positionsList: positionsList) methodChannelManager?.shuffleChanged(shuffleIsActive: queueManager.isShuffleModeEnabled) } - func reorder(fromIndex: Int, toIndex: Int, positionsList: [[String: Int]]) { - queueManager.reorder(fromIndex: fromIndex, toIndex: toIndex) - } - - func nextTrack(from:String) { - smPlayer.pause() - Logger.debugLog("#print nextTrack \(from)") - if let currentItem = smPlayer.currentItem { - queueManager.historyQueue.append(currentItem) - } - - if(smPlayer.currentItem == fullQueue.last && smPlayer.repeatMode == .REPEAT_MODE_ALL){ - playFromQueue(position: 0) - } - smPlayer.advanceToNextItem() - smPlayer.seek(to: CMTime.zero) - insertIntoPlayerIfNeeded() - smPlayer.play() - printStatus(from:"NEXT") - } - - func previousTrack() { - smPlayer.pause() - - guard let lastHistoryItem = queueManager.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) - queueManager.futureQueue.insert(lastItemInPlayer, at: 0) - } - - smPlayer.insert(lastHistoryItem, after: currentItem) - smPlayer.advanceToNextItem() - smPlayer.insert(currentItem, after: smPlayer.currentItem) - - smPlayer.seek(to: CMTime.zero) - insertIntoPlayerIfNeeded() - smPlayer.play() - printStatus(from:"previousTrack") - } - func setNowPlaying(){ - NowPlayingCenter.set(item: getCurrentPlaylistItem()) - } - - private func insertIntoPlayerIfNeeded() { - let maxTotalItems = Constants.maxTotalItems - let itemsToAdd = min(maxTotalItems - smPlayer.items().count, queueManager.futureQueue.count) - - for _ in 0.. \(String(describing: smPlayer.items()))") - smPlayer.insert(item, after: nil) - queueManager.futureQueue.removeFirst() - } - } - Logger.debugLog("#NATIVE LOGS insertIntoPlayerIfNeeded ==> \(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() - queueManager.historyQueue.removeAll() - queueManager.futureQueue.removeAll() - queueManager.originalQueue.removeAll() - queueManager.shuffledQueue.removeAll() + queueManager.removeAll() methodChannelManager?.notifyPlayerStateChange(state:PlayerState.idle) } @@ -281,47 +211,6 @@ public class SMPlayer : NSObject { } ) } - 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) { - let startTime = CFAbsoluteTimeGetCurrent() - guard currentQueue.count > 0 else { return } - var position = positionArg - queueManager.historyQueue.removeAll() - queueManager.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() - queueManager.futureQueue.append(currentQueue[position]) - } - - for (index, item) in currentQueue.enumerated() { - if(index != position){ - if index < position { - queueManager.historyQueue.append(item) - } else { - queueManager.futureQueue.append(item) - } - } - } - insertIntoPlayerIfNeeded() - completion?() - - let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime - print("TIME distributeItemsInRightQueue execution time: \(timeElapsed) seconds") - } - func updateMediaUri(id: Int, uri: String?){ var fullQueueUpdated = fullQueue if let index = fullQueue.firstIndex(where: { $0.playlistItem?.mediaId == id }){ @@ -333,73 +222,29 @@ public class SMPlayer : NSObject { 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) { - 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) + queueManager.distributeItemsInRightQueue(currentQueue: fullQueueUpdated) } - }) - if(loadOnly){ - pause() - shouldNotifyTransition = false - }else{ - play() } - listeners?.addMediaChangeObserver() } func enableCommands(){ nowPlayingInfoManager.enableCommands() } - - func printStatus(from:String) { - Logger.debugLog("QueueActivity #################################################") - Logger.debugLog("QueueActivity \(from) ") - Logger.debugLog("QueueActivity Current Index: \(String(describing: currentIndex))") - Logger.debugLog("QueueActivity ------------------------------------------") - Logger.debugLog("QueueActivity printStatus History: \(queueManager.historyQueue.count) items") - - for item in queueManager.historyQueue { - Logger.debugLog("QueueActivity printStatus History: \(String(describing: item.playlistItem?.title))") - } - Logger.debugLog("QueueActivity printStatus ------------------------------------------") - Logger.debugLog("QueueActivity printStatus futureQueue Items: \(queueManager.futureQueue.count) items") - - for item in queueManager.futureQueue { - Logger.debugLog("QueueActivity printStatus Upcoming: \(String(describing: item.playlistItem?.title))") - } - Logger.debugLog("QueueActivity printStatus ------------------------------------------") - Logger.debugLog("QueueActivity printStatus AVQueuePlayer items: \(smPlayer.items().count)") - - for item in smPlayer.items() { - Logger.debugLog("QueueActivity printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") - } - Logger.debugLog("QueueActivity printStatus #################################################") - - } - //override automatic next @objc func itemDidFinishPlaying(_ notification: Notification) { 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(from:"REPEAT_MODE_ALL") case .REPEAT_MODE_ONE: seekToPosition(position: 0) case .REPEAT_MODE_OFF: - nextTrack(from: "REPEAT_MODE_OFF") + queueManager.nextTrack(from: "REPEAT_MODE_OFF") } play() } @@ -408,6 +253,15 @@ public class SMPlayer : NSObject { notificationManager.removeAllObservers() // Remover outros observadores } + + private func notifyMediaChangedIfNeeded() { + let isNotFirstItem = smPlayer.currentItem != fullQueue.first + let hasHistory = queueManager.historyQueue.count > 0 + + if isNotFirstItem && hasHistory { + methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) + } + } } extension AVPlayerItem { diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index 224753e7..4df3fa54 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -1,11 +1,18 @@ import Foundation import AVFoundation -public class SMPlayerListeners : NSObject { +private enum ObserverKey: String { + case status, isPlaybackBufferEmpty, isPlaybackLikelyToKeepUp, currentItem, reasonForWaitingToPlay, timeControlStatus +} + +public class SMPlayerListeners: NSObject { let smPlayer: AVQueuePlayer - let methodChannelManager: MethodChannelManager? + weak var methodChannelManager: MethodChannelManager? - var onMediaChanged: (() -> Void)? + var onMediaChanged: ((Bool) -> Void)? + private var itemObservations = [NSKeyValueObservation]() + private var playerObservations = [NSKeyValueObservation]() + private var lastState = PlayerState.idle init(smPlayer: AVQueuePlayer, methodChannelManager: MethodChannelManager?) { self.smPlayer = smPlayer @@ -14,26 +21,19 @@ public class SMPlayerListeners : NSObject { addPlayerObservers() } - 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? - - private var lastState = PlayerState.idle - - func addItemsObservers() { removeItemObservers() guard let currentItem = smPlayer.currentItem else { return } - statusChange = currentItem.observe(\.status, options: [.new, .old]) { (playerItem, change) in + + let statusObs = currentItem.observe(\AVPlayerItem.status, options: [.new, .old]) { [weak self] playerItem, _ in + guard let self = self else { return } switch playerItem.status { case .failed: if let error = playerItem.error { - print("#NATIVE LOGS ==> ERROR: \(String(describing: playerItem.error))") - self.methodChannelManager?.notifyError(error: "UNKNOWN ERROR") + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] ERROR: \(error.localizedDescription)") + self.methodChannelManager?.notifyError(error: error.localizedDescription) + } else { + self.methodChannelManager?.notifyError(error: "Unknown error") } case .readyToPlay: self.notifyPlayerStateChange(state: PlayerState.stateReady) @@ -43,98 +43,108 @@ public class SMPlayerListeners : NSObject { break } } + itemObservations.append(statusObs) - - loading = currentItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old]) { [weak self] (new, old) in + let loadingObs = currentItem.observe(\AVPlayerItem.isPlaybackBufferEmpty, options: [.new, .old]) { [weak self] _, _ in guard let self = self else { return } - print("#NATIVE LOGS ==> Listeners - observer - loading") - notifyPlayerStateChange(state: PlayerState.buffering) + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Buffering (isPlaybackBufferEmpty)") + self.notifyPlayerStateChange(state: PlayerState.buffering) } + itemObservations.append(loadingObs) - loaded = currentItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { (player, _) in - print("#NATIVE LOGS ==> Listeners - observer - loaded") + let loadedObs = currentItem.observe(\AVPlayerItem.isPlaybackLikelyToKeepUp, options: [.new]) { [weak self] _, _ in + guard let self = self else { return } + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Loaded (isPlaybackLikelyToKeepUp)") } + itemObservations.append(loadedObs) } - func notifyPlayerStateChange(state: PlayerState){ - if(lastState != state){ - self.methodChannelManager?.notifyPlayerStateChange(state: state) + func notifyPlayerStateChange(state: PlayerState) { + if lastState != state { + methodChannelManager?.notifyPlayerStateChange(state: state) lastState = state } - } + } - - func addMediaChangeObserver(){ - mediaChange = smPlayer.observe(\.currentItem, options: [.new, .old]) { [weak self] (player, change) in + func addMediaChangeObserver() { + removeMediaChangeObserver() + let mediaChangeObs = smPlayer.observe(\AVQueuePlayer.currentItem, options: [.new, .old]) { [weak self] _, change in guard let self = self else { return } let oldItemExists = change.oldValue != nil - print("#NATIVE LOGS ==> onMediaChanged: \(oldItemExists)") - + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Media changed. Old item exists: \(oldItemExists)") if let newItem = change.newValue, newItem != change.oldValue { - self.onMediaChanged?() + self.onMediaChanged?(true) self.addItemsObservers() } } + playerObservations.append(mediaChangeObs) } + func addPlayerObservers() { addMediaChangeObserver() let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in + smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ 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) + if let playlistItem = currentItem.playlistItem { + NowPlayingCenter.update(item: playlistItem, rate: 1.0, position: position, duration: duration) + } } } } - notPlayingReason = smPlayer.observe(\.reasonForWaitingToPlay, options: [.new]) { (playerItem, change) in - switch self.smPlayer.reasonForWaitingToPlay { + let notPlayingReasonObs = smPlayer.observe(\AVQueuePlayer.reasonForWaitingToPlay, options: [.new]) { [weak self] player, _ in + guard let self = self else { return } + switch player.reasonForWaitingToPlay { case .evaluatingBufferingRate: - print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - evaluatingBufferingRate") + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Reason: evaluatingBufferingRate") case .toMinimizeStalls: - print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - toMinimizeStalls") + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Reason: toMinimizeStalls") case .noItemToPlay: - print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - noItemToPlay") + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Reason: noItemToPlay") default: - print("#NATIVE LOGS ==> Listeners reasonForWaitingToPlay - default") + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Reason: default") } } + playerObservations.append(notPlayingReasonObs) - playback = smPlayer.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] (player, change) in + let playbackObs = smPlayer.observe(\AVQueuePlayer.timeControlStatus, options: [.new, .old]) { [weak self] player, _ in guard let self = self else { return } switch player.timeControlStatus { case .playing: - notifyPlayerStateChange(state: PlayerState.playing) - print("#NATIVE LOGS ==> Listeners - Playing") + self.notifyPlayerStateChange(state: PlayerState.playing) + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Playing") case .paused: - notifyPlayerStateChange(state: PlayerState.paused) - print("#NATIVE LOGS ==> Listeners - Paused") + self.notifyPlayerStateChange(state: PlayerState.paused) + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Paused") case .waitingToPlayAtSpecifiedRate: - notifyPlayerStateChange(state: PlayerState.buffering) - print("#NATIVE LOGS ==> Listeners - Buffering") + self.notifyPlayerStateChange(state: PlayerState.buffering) + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Buffering") @unknown default: break } } + playerObservations.append(playbackObs) } func removeItemObservers() { - statusChange?.invalidate() - loading?.invalidate() - loaded?.invalidate() - - statusChange = nil - loading = nil - loaded = nil - + itemObservations.forEach { $0.invalidate() } + itemObservations.removeAll() removeErrorObserver() } + func removeMediaChangeObserver() { + playerObservations = playerObservations.filter { obs in + obs.invalidate() + return false + } + } + func removeErrorObserver() { if let currentItem = smPlayer.currentItem { NotificationCenter.default.removeObserver( @@ -145,18 +155,15 @@ public class SMPlayerListeners : NSObject { } } - + /// Removes all player observers. func removePlayerObservers() { - notPlayingReason?.invalidate() - playback?.invalidate() - mediaChange?.invalidate() - mediaChange = nil - notPlayingReason = nil - playback = nil + playerObservations.forEach { $0.invalidate() } + playerObservations.removeAll() + removeItemObservers() } deinit { removePlayerObservers() - removeItemObservers() } } + From 970dbec34e4986d5de9925623a8434ef6ed2d827 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 3 Jun 2025 14:12:14 -0300 Subject: [PATCH 28/34] improve performance --- .../ios/Classes/NowPlayingInfoManager.swift | 4 +- .../player/ios/Classes/PlayerPlugin.swift | 2 +- .../player/ios/Classes/QueueManager.swift | 104 ++++++++++++------ packages/player/ios/Classes/SMPlayer.swift | 44 +------- 4 files changed, 78 insertions(+), 76 deletions(-) diff --git a/packages/player/ios/Classes/NowPlayingInfoManager.swift b/packages/player/ios/Classes/NowPlayingInfoManager.swift index 48b66297..12b18300 100644 --- a/packages/player/ios/Classes/NowPlayingInfoManager.swift +++ b/packages/player/ios/Classes/NowPlayingInfoManager.swift @@ -47,6 +47,8 @@ class NowPlayingInfoManager { let commandCenter = MPRemoteCommandCenter.shared() commandCenter.nextTrackCommand.isEnabled = true commandCenter.previousTrackCommand.isEnabled = true + commandCenter.pauseCommand.isEnabled = true + commandCenter.playCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.isEnabled = true } @@ -66,4 +68,4 @@ class NowPlayingInfoManager { MPNowPlayingInfoCenter.default().nowPlayingInfo = nil removeNotification() } -} \ No newline at end of file +} diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index ae0b3585..60a1e30f 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -90,7 +90,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { 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": diff --git a/packages/player/ios/Classes/QueueManager.swift b/packages/player/ios/Classes/QueueManager.swift index fca28108..af8781ca 100644 --- a/packages/player/ios/Classes/QueueManager.swift +++ b/packages/player/ios/Classes/QueueManager.swift @@ -1,10 +1,11 @@ import AVFoundation class QueueManager { - var historyQueue: [AVPlayerItem] = [] - var futureQueue: [AVPlayerItem] = [] - var originalQueue: [AVPlayerItem] = [] - var shuffledQueue: [AVPlayerItem] = [] + var historyQueue: [PlaylistItem] = [] + var futureQueue: [PlaylistItem] = [] + var originalQueue: [PlaylistItem] = [] + var shuffledQueue: [PlaylistItem] = [] + var shuffledIndices: [Int] = [] var isShuffleModeEnabled: Bool = false private let smPlayer: AVQueuePlayer @@ -15,12 +16,17 @@ class QueueManager { self.maxTotalItems = maxTotalItems } - var fullQueue: [AVPlayerItem] { - return historyQueue + smPlayer.items() + futureQueue + + var mirrorPlayerQueue: [PlaylistItem] { + return smPlayer.items().compactMap { $0.playlistItem} + } + + var fullQueue: [PlaylistItem] { + return historyQueue + mirrorPlayerQueue + futureQueue } var currentIndex: Int { - guard let currentItem = smPlayer.currentItem else { + guard let currentItem = smPlayer.currentItem?.playlistItem else { return 0 } return fullQueue.firstIndex(of: currentItem) ?? 0 @@ -69,14 +75,14 @@ class QueueManager { } } - func distributeItemsInRightQueue(currentQueue: [AVPlayerItem], keepFirst: Bool = true, positionArg: Int = -1, completionHandler completion: (() -> Void)? = nil) { + func distributeItemsInRightQueue(currentQueue: [PlaylistItem], 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 + position = (smPlayer.currentItem?.playlistItem != nil ? currentQueue.firstIndex(of: smPlayer.currentItem!.playlistItem!) : -1)! let itemsToRemove = smPlayer.items().dropFirst() for item in itemsToRemove { smPlayer.remove(item) @@ -105,7 +111,9 @@ class QueueManager { let itemsToAdd = min(maxTotalItems - smPlayer.items().count, futureQueue.count) for _ in 0.. AVPlayerItem?, cookie: String?) { - var fullQueueUpdated = fullQueue - if let index = fullQueue.firstIndex(where: { $0.playlistItem?.mediaId == id }) { - let oldItem = fullQueueUpdated[index] - if let playerItem = createPlayerItemFromUri(uri, oldItem.playlistItem?.fallbackUrl, cookie) { - playerItem.playlistItem = oldItem.playlistItem - fullQueueUpdated[index] = playerItem - distributeItemsInRightQueue(currentQueue: fullQueueUpdated) - } - } + func updateMediaUri(id: Int, uri: String?) { + guard let newUri = uri else { return } + guard let index = fullQueue.firstIndex(where: { $0.mediaId == id }) else { return } + let oldItem = fullQueue[index] + if oldItem.url == newUri { return } + let updatedItem = createUpdatedPlaylistItem(from: oldItem, newUri: newUri) + fullQueue[index] = updatedItem + distributeItemsInRightQueue(currentQueue: updatedQueue) + } + + private func createUpdatedPlaylistItem(from oldItem: PlaylistItem, newUri: String) -> PlaylistItem { + return 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, seekToPosition: ((Int) -> Void)? = nil, pause: (() -> Void)? = nil, play: (() -> Void)? = nil, notifyCurrentMediaIndex: ((Int) -> Void)? = nil, addMediaChangeObserver: (() -> Void)? = nil) { @@ -152,21 +173,21 @@ class QueueManager { // addMediaChangeObserver?() } - func nextTrack(from: String, playFromQueue: ((Int) -> Void)? = nil, play: (() -> Void)? = nil) { + func nextTrack(from: String, playFromQueue: ((Int) -> Void)? = nil) { smPlayer.pause() - if let currentItem = smPlayer.currentItem { + if let currentItem = smPlayer.currentItem?.playlistItem{ historyQueue.append(currentItem) } - if smPlayer.currentItem == fullQueue.last && smPlayer.repeatMode == .REPEAT_MODE_ALL { + if smPlayer.currentItem?.playlistItem == fullQueue.last && smPlayer.repeatMode == .REPEAT_MODE_ALL { playFromQueue?(0) } smPlayer.advanceToNextItem() smPlayer.seek(to: CMTime.zero) insertIntoPlayerIfNeeded() - play?() + smPlayer.play() } - func previousTrack(seekToPosition: ((Int) -> Void)? = nil, play: (() -> Void)? = nil) { + func previousTrack(seekToPosition: ((Int) -> Void)? = nil) { smPlayer.pause() guard let lastHistoryItem = historyQueue.popLast() else { seekToPosition?(0) @@ -176,14 +197,15 @@ class QueueManager { guard let lastItemInPlayer = smPlayer.items().last else { return } if currentItem != lastItemInPlayer { smPlayer.remove(lastItemInPlayer) - futureQueue.insert(lastItemInPlayer, at: 0) + futureQueue.insert(lastItemInPlayer.playlistItem!, at: 0) } - smPlayer.insert(lastHistoryItem, after: currentItem) + let historyAVPlayerItem = createPlayerItemFromUri(lastHistoryItem.url, fallbackUrl:lastHistoryItem.fallbackUrl,cookie:lastHistoryItem.cookie) + smPlayer.insert(historyAVPlayerItem!, after: currentItem) smPlayer.advanceToNextItem() smPlayer.insert(currentItem, after: smPlayer.currentItem) smPlayer.seek(to: CMTime.zero) insertIntoPlayerIfNeeded() - play?() + smPlayer.play() } func getCurrentPlaylistItem() -> PlaylistItem? { @@ -201,13 +223,13 @@ class QueueManager { Logger.debugLog("QueueActivity printStatus History: \(historyQueue.count) items") for item in historyQueue { - Logger.debugLog("QueueActivity printStatus History: \(String(describing: item.playlistItem?.title))") + Logger.debugLog("QueueActivity printStatus History: \(String(describing: item.title))") } Logger.debugLog("QueueActivity printStatus ------------------------------------------") Logger.debugLog("QueueActivity printStatus futureQueue Items: \(futureQueue.count) items") for item in futureQueue { - Logger.debugLog("QueueActivity printStatus Upcoming: \(String(describing: item.playlistItem?.title))") + Logger.debugLog("QueueActivity printStatus Upcoming: \(String(describing: item.title))") } Logger.debugLog("QueueActivity printStatus ------------------------------------------") Logger.debugLog("QueueActivity printStatus AVQueuePlayer items: \(smPlayer.items().count)") @@ -218,7 +240,25 @@ class QueueManager { Logger.debugLog("QueueActivity printStatus #################################################") } - func enqueue(item: AVPlayerItem) { + func enqueue(item: PlaylistItem) { futureQueue.append(item) } -} + + private func createPlayerItem(with url: URL, cookie: String?) -> AVPlayerItem { + let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie ?? ""]] + return AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) + } + + private func createLocalPlayerItem(with path: String) -> AVPlayerItem { + return AVPlayerItem(asset: AVAsset(url: NSURL(fileURLWithPath: path) as URL)) + } + + func createPlayerItemFromUri(_ uri: String?, fallbackUrl: String?, cookie: String?) -> AVPlayerItem? { + if uri?.contains("https") ?? true { + guard let url = URL(string: (uri ?? fallbackUrl!) ?? "") else { return nil } + return createPlayerItem(with: url, cookie: cookie) + } else { + return createLocalPlayerItem(with: uri!) + } + } +} diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 982dd009..cc0eadec 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -21,7 +21,7 @@ public class SMPlayer : NSObject { static let defaultTimescale: CMTimeScale = 60000 } - var fullQueue: [AVPlayerItem] { + var fullQueue: [PlaylistItem] { return queueManager.fullQueue } @@ -136,38 +136,15 @@ public class SMPlayer : NSObject { nowPlayingInfoManager.clearNowPlayingInfo() } - private func createPlayerItem(with url: URL, cookie: String?) -> AVPlayerItem { - let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie ?? ""]] - return AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) - } - - private func createLocalPlayerItem(with path: String) -> AVPlayerItem { - return AVPlayerItem(asset: AVAsset(url: NSURL(fileURLWithPath: path) as URL)) - } - - private func createPlayerItemFromUri(_ uri: String?, fallbackUrl: String?, cookie: String?) -> AVPlayerItem? { - if uri?.contains("https") ?? true { - guard let url = URL(string: (uri ?? fallbackUrl!) ?? "") else { return nil } - return createPlayerItem(with: url, cookie: cookie) - } else { - return createLocalPlayerItem(with: uri!) - } - } - func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { - var playerItem: AVPlayerItem? guard let message = MessageBuffer.shared.receive() else { return } if(!cookie.isEmpty){ self.cookie = cookie } let isFirstBatch = self.smPlayer.items().count == 0 for media in message { - playerItem = createPlayerItemFromUri(media.url, fallbackUrl: nil, cookie: self.cookie) media.cookie = cookie - if playerItem != nil { - playerItem!.playlistItem = media - queueManager.enqueue(item: playerItem!) - } + queueManager.enqueue(item: media) } queueManager.insertIntoPlayerIfNeeded() if autoPlay && isFirstBatch { @@ -175,7 +152,6 @@ public class SMPlayer : NSObject { self.setNowPlaying() } self.enableCommands() - //TODO: precisa adicionar em todos os batches? listeners?.addPlayerObservers() } @@ -211,22 +187,6 @@ public class SMPlayer : NSObject { } ) } - func updateMediaUri(id: Int, uri: String?){ - var fullQueueUpdated = fullQueue - if let index = fullQueue.firstIndex(where: { $0.playlistItem?.mediaId == id }){ - let oldItem = fullQueueUpdated[index] - if let playerItem = createPlayerItemFromUri(uri, fallbackUrl: oldItem.playlistItem?.fallbackUrl, cookie: oldItem.playlistItem?.cookie) { - 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)") - } - queueManager.distributeItemsInRightQueue(currentQueue: fullQueueUpdated) - } - } - } - func enableCommands(){ nowPlayingInfoManager.enableCommands() } From ffa77a1ed1e77b3803c2d92686113dc20d2a76fd Mon Sep 17 00:00:00 2001 From: lstonussi Date: Tue, 3 Jun 2025 14:28:55 -0300 Subject: [PATCH 29/34] remove unecessary stuffs --- .../player/ios/Classes/MessageBuffer.swift | 71 ------------------- .../player/ios/Classes/PlayerPlugin.swift | 3 +- .../player/ios/Classes/QueueManager.swift | 3 +- packages/player/ios/Classes/SMPlayer.swift | 3 +- 4 files changed, 4 insertions(+), 76 deletions(-) delete mode 100644 packages/player/ios/Classes/MessageBuffer.swift 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/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index 60a1e30f..267c2423 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -29,8 +29,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { let autoPlay = batch["autoPlay"] as? Bool ?? false let cookie = batch["cookie"] as? String ?? "" if let mediaList = convertToMedia(mediaArray: listMedia) { - MessageBuffer.shared.send(mediaList) - smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie) + smPlayer?.enqueue(medias: mediaList, autoPlay: autoPlay, cookie: cookie) } } result(NSNumber(value: true)) diff --git a/packages/player/ios/Classes/QueueManager.swift b/packages/player/ios/Classes/QueueManager.swift index af8781ca..a8103cbe 100644 --- a/packages/player/ios/Classes/QueueManager.swift +++ b/packages/player/ios/Classes/QueueManager.swift @@ -138,7 +138,8 @@ class QueueManager { let oldItem = fullQueue[index] if oldItem.url == newUri { return } let updatedItem = createUpdatedPlaylistItem(from: oldItem, newUri: newUri) - fullQueue[index] = updatedItem + var updatedQueue = fullQueue + updatedQueue[index] = updatedItem distributeItemsInRightQueue(currentQueue: updatedQueue) } diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index cc0eadec..c91fc1dd 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -137,12 +137,11 @@ public class SMPlayer : NSObject { } func enqueue(medias: [PlaylistItem], autoPlay: Bool, cookie: String) { - guard let message = MessageBuffer.shared.receive() else { return } if(!cookie.isEmpty){ self.cookie = cookie } let isFirstBatch = self.smPlayer.items().count == 0 - for media in message { + for media in medias { media.cookie = cookie queueManager.enqueue(item: media) } From 26f4a3a80e025bef58f64ea8856b2e21a8d747df Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 4 Jun 2025 13:02:52 -0300 Subject: [PATCH 30/34] fix observers --- packages/player/ios/Classes/Logger.swift | 6 +- .../ios/Classes/NowPlayingInfoManager.swift | 4 +- .../player/ios/Classes/PlayerPlugin.swift | 6 +- .../player/ios/Classes/QueueManager.swift | 77 +++++++++++++------ packages/player/ios/Classes/SMPlayer.swift | 50 ++++-------- .../ios/Classes/SMPlayerListeners.swift | 71 ++++++++++++----- 6 files changed, 128 insertions(+), 86 deletions(-) diff --git a/packages/player/ios/Classes/Logger.swift b/packages/player/ios/Classes/Logger.swift index 859dc1db..4cb703d6 100644 --- a/packages/player/ios/Classes/Logger.swift +++ b/packages/player/ios/Classes/Logger.swift @@ -2,8 +2,8 @@ import Foundation public class Logger { public static func debugLog(_ message: String) { - #if DEBUG +// #if DEBUG print(message) - #endif +// #endif } -} \ No newline at end of file +} diff --git a/packages/player/ios/Classes/NowPlayingInfoManager.swift b/packages/player/ios/Classes/NowPlayingInfoManager.swift index 12b18300..4f0912ec 100644 --- a/packages/player/ios/Classes/NowPlayingInfoManager.swift +++ b/packages/player/ios/Classes/NowPlayingInfoManager.swift @@ -7,6 +7,8 @@ class NowPlayingInfoManager { 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 @@ -57,8 +59,6 @@ class NowPlayingInfoManager { commandCenter.nextTrackCommand.isEnabled = false commandCenter.previousTrackCommand.isEnabled = false commandCenter.changePlaybackPositionCommand.isEnabled = false - commandCenter.playCommand.removeTarget(nil) - commandCenter.pauseCommand.removeTarget(nil) MPNowPlayingInfoCenter.default().nowPlayingInfo = nil try? AVAudioSession.sharedInstance().setActive(false) UIApplication.shared.endReceivingRemoteControlEvents() diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index 267c2423..0de18345 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -50,10 +50,10 @@ public class PlayerPlugin: NSObject, FlutterPlugin { 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]{ @@ -95,7 +95,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { 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: diff --git a/packages/player/ios/Classes/QueueManager.swift b/packages/player/ios/Classes/QueueManager.swift index a8103cbe..5c1c7adb 100644 --- a/packages/player/ios/Classes/QueueManager.swift +++ b/packages/player/ios/Classes/QueueManager.swift @@ -9,11 +9,19 @@ class QueueManager { var shuffledIndices: [Int] = [] var isShuffleModeEnabled: Bool = false private let smPlayer: AVQueuePlayer - private let maxTotalItems: Int + + private let listeners: SMPlayerListeners + private let methodChannelManager: MethodChannelManager + + private enum Constants { + static let maxTotalItems = 5 + static let defaultTimescale: CMTimeScale = 60000 + } - init(smPlayer: AVQueuePlayer, maxTotalItems: Int = 5) { + init(smPlayer: AVQueuePlayer, listeners: SMPlayerListeners, methodChannelManager: MethodChannelManager) { self.smPlayer = smPlayer - self.maxTotalItems = maxTotalItems + self.listeners = listeners + self.methodChannelManager = methodChannelManager } @@ -25,6 +33,15 @@ class QueueManager { return 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, completionHandler: { completed in + if completed { + self.methodChannelManager.notifyPlayerStateChange(state: PlayerState.seekEnd) + } + } ) + } + var currentIndex: Int { guard let currentItem = smPlayer.currentItem?.playlistItem else { return 0 @@ -108,7 +125,7 @@ class QueueManager { } func insertIntoPlayerIfNeeded() { - let itemsToAdd = min(maxTotalItems - smPlayer.items().count, futureQueue.count) + let itemsToAdd = min(Constants.maxTotalItems - smPlayer.items().count, futureQueue.count) for _ in 0.. Void)? = nil, pause: (() -> Void)? = nil, play: (() -> Void)? = nil, notifyCurrentMediaIndex: ((Int) -> Void)? = nil, addMediaChangeObserver: (() -> Void)? = nil) { + func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false) { distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position, completionHandler: { - notifyCurrentMediaIndex?(self.currentIndex) + self.methodChannelManager.currentMediaIndex(index: self.currentIndex) if timePosition > 0 { - seekToPosition?(timePosition) + self.seekToTimePosition(position:timePosition) } }) if loadOnly { - pause?() + smPlayer.pause() // shouldNotifyTransition = false } else { - play?() + smPlayer.play() } -// addMediaChangeObserver?() } - func nextTrack(from: String, playFromQueue: ((Int) -> Void)? = nil) { + func nextTrack(from: String) { smPlayer.pause() if let currentItem = smPlayer.currentItem?.playlistItem{ historyQueue.append(currentItem) } if smPlayer.currentItem?.playlistItem == fullQueue.last && smPlayer.repeatMode == .REPEAT_MODE_ALL { - playFromQueue?(0) + playFromQueue(position:0) } smPlayer.advanceToNextItem() - smPlayer.seek(to: CMTime.zero) + seekToTimePosition(position:0) insertIntoPlayerIfNeeded() smPlayer.play() } - func previousTrack(seekToPosition: ((Int) -> Void)? = nil) { + func previousTrack() { smPlayer.pause() guard let lastHistoryItem = historyQueue.popLast() else { - seekToPosition?(0) + seekToTimePosition(position:0) + smPlayer.play() return } - guard let currentItem = smPlayer.currentItem else { return } - guard let lastItemInPlayer = smPlayer.items().last else { 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.playlistItem!, at: 0) } + let historyAVPlayerItem = createPlayerItemFromUri(lastHistoryItem.url, fallbackUrl:lastHistoryItem.fallbackUrl,cookie:lastHistoryItem.cookie) - smPlayer.insert(historyAVPlayerItem!, after: currentItem) - smPlayer.advanceToNextItem() - smPlayer.insert(currentItem, after: smPlayer.currentItem) - smPlayer.seek(to: CMTime.zero) + + if historyAVPlayerItem != nil { + historyAVPlayerItem!.playlistItem = lastHistoryItem + smPlayer.insert(historyAVPlayerItem!, after: currentItem) + smPlayer.advanceToNextItem() + smPlayer.insert(currentItem, after: smPlayer.currentItem) + } + + seekToTimePosition(position:0) insertIntoPlayerIfNeeded() + listeners.addItemsObservers() smPlayer.play() } @@ -256,7 +287,7 @@ class QueueManager { func createPlayerItemFromUri(_ uri: String?, fallbackUrl: String?, cookie: String?) -> AVPlayerItem? { if uri?.contains("https") ?? true { - guard let url = URL(string: (uri ?? fallbackUrl!) ?? "") else { return nil } + guard let url = URL(string: (uri ?? fallbackUrl) ?? "") else { return nil } return createPlayerItem(with: url, cookie: cookie) } else { return createLocalPlayerItem(with: uri!) diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index c91fc1dd..2494be54 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -7,19 +7,15 @@ public class SMPlayer : NSObject { var methodChannelManager: MethodChannelManager? private var cookie: String = "" //Queue handle - private var smPlayer: AVQueuePlayer - var queueManager: QueueManager + var smPlayer: AVQueuePlayer + var queueManager: QueueManager! private var listeners: SMPlayerListeners? = nil // Transition Control var areNotificationCommandsEnabled: Bool = true private var notificationManager: NotificationManager! private var nowPlayingInfoManager: NowPlayingInfoManager! - - private enum Constants { - static let maxTotalItems = 5 - static let defaultTimescale: CMTimeScale = 60000 - } + var fullQueue: [PlaylistItem] { return queueManager.fullQueue @@ -31,13 +27,14 @@ public class SMPlayer : NSObject { init(methodChannelManager: MethodChannelManager?) { smPlayer = AVQueuePlayer() - queueManager = QueueManager(smPlayer: smPlayer, maxTotalItems: Constants.maxTotalItems) - super.init() - nowPlayingInfoManager = NowPlayingInfoManager() - notificationManager = NotificationManager(target: self) self.methodChannelManager = methodChannelManager listeners = SMPlayerListeners(smPlayer:smPlayer,methodChannelManager:methodChannelManager) + 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 } @@ -56,11 +53,11 @@ public class SMPlayer : NSObject { } nowPlayingInfoManager.setupNowPlayingInfoCenter( areNotificationCommandsEnabled: { [weak self] in self?.areNotificationCommandsEnabled ?? true }, - play: { [weak self] in self?.play() }, - pause: { [weak self] in self?.pause() }, + play: { [weak self] in self?.smPlayer.play() }, + pause: { [weak self] in self?.smPlayer.pause() }, nextTrack: { [weak self] in self?.queueManager.nextTrack(from: "commandCenter.nextTrackCommand") }, previousTrack: { [weak self] in self?.queueManager.previousTrack() }, - seekToPosition: { [weak self] pos in self?.seekToPosition(position: pos) } + seekToPosition: { [weak self] pos in self?.queueManager.seekToTimePosition(position: pos) } ) _ = AudioSessionManager.activeSession() } @@ -78,7 +75,7 @@ public class SMPlayer : NSObject { if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) if options.contains(.shouldResume) { - play() + smPlayer.play() } } @unknown default: @@ -86,10 +83,6 @@ public class SMPlayer : NSObject { } } - func pause() { - smPlayer.pause() - } - func addEndPlaybackObserver() { guard let currentItem = smPlayer.currentItem else { return } notificationManager.addEndPlaybackObserver(selector: #selector(itemDidFinishPlaying(_:)), for: currentItem) @@ -151,7 +144,6 @@ public class SMPlayer : NSObject { self.setNowPlaying() } self.enableCommands() - listeners?.addPlayerObservers() } func toggleShuffle(positionsList: [[String: Int]]) { @@ -173,18 +165,7 @@ public class SMPlayer : NSObject { nowPlayingInfoManager.removeNotification() } - func play(){ - smPlayer.play() - } - - func seekToPosition(position:Int){ - let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: Constants.defaultTimescale) - smPlayer.currentItem?.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { completed in - if completed { - self.methodChannelManager?.notifyPlayerStateChange(state: PlayerState.seekEnd) - } - } ) - } + func enableCommands(){ nowPlayingInfoManager.enableCommands() @@ -201,16 +182,15 @@ public class SMPlayer : NSObject { } queueManager.nextTrack(from:"REPEAT_MODE_ALL") case .REPEAT_MODE_ONE: - seekToPosition(position: 0) + queueManager.seekToTimePosition(position: 0) case .REPEAT_MODE_OFF: queueManager.nextTrack(from: "REPEAT_MODE_OFF") } - play() + smPlayer.play() } private func removeAllObservers() { notificationManager.removeAllObservers() - // Remover outros observadores } private func notifyMediaChangedIfNeeded() { diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index 4df3fa54..fb4c71be 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -12,6 +12,7 @@ public class SMPlayerListeners: NSObject { var onMediaChanged: ((Bool) -> Void)? private var itemObservations = [NSKeyValueObservation]() private var playerObservations = [NSKeyValueObservation]() + private var periodicTimeObserver: Any? private var lastState = PlayerState.idle init(smPlayer: AVQueuePlayer, methodChannelManager: MethodChannelManager?) { @@ -53,10 +54,28 @@ public class SMPlayerListeners: NSObject { itemObservations.append(loadingObs) let loadedObs = currentItem.observe(\AVPlayerItem.isPlaybackLikelyToKeepUp, options: [.new]) { [weak self] _, _ in - guard let self = self else { return } + guard self != nil else { return } Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Loaded (isPlaybackLikelyToKeepUp)") } itemObservations.append(loadedObs) + + addErrorObserver(for: currentItem) + } + + private func addErrorObserver(for item: AVPlayerItem) { + NotificationCenter.default.addObserver( + self, + selector: #selector(handlePlayerItemError(_:)), + name: .AVPlayerItemFailedToPlayToEndTime, + object: item + ) + } + + @objc private func handlePlayerItemError(_ notification: Notification) { + if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error { + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Player Item Error: \(error.localizedDescription)") + methodChannelManager?.notifyError(error: error.localizedDescription) + } } func notifyPlayerStateChange(state: PlayerState) { @@ -82,24 +101,10 @@ public class SMPlayerListeners: NSObject { func addPlayerObservers() { addMediaChangeObserver() - - let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ 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) - if let playlistItem = currentItem.playlistItem { - NowPlayingCenter.update(item: playlistItem, rate: 1.0, position: position, duration: duration) - } - } - } - } + addPeriodicTimeObserver() let notPlayingReasonObs = smPlayer.observe(\AVQueuePlayer.reasonForWaitingToPlay, options: [.new]) { [weak self] player, _ in - guard let self = self else { return } + guard self != nil else { return } switch player.reasonForWaitingToPlay { case .evaluatingBufferingRate: Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Reason: evaluatingBufferingRate") @@ -132,6 +137,31 @@ public class SMPlayerListeners: NSObject { playerObservations.append(playbackObs) } + private func addPeriodicTimeObserver() { + removePeriodicTimeObserver() + let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + periodicTimeObserver = smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ 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) + if let playlistItem = currentItem.playlistItem { + NowPlayingCenter.update(item: playlistItem, rate: 1.0, position: position, duration: duration) + } + } + } + } + } + + private func removePeriodicTimeObserver() { + if let observer = periodicTimeObserver { + smPlayer.removeTimeObserver(observer) + periodicTimeObserver = nil + } + } + func removeItemObservers() { itemObservations.forEach { $0.invalidate() } itemObservations.removeAll() @@ -139,9 +169,9 @@ public class SMPlayerListeners: NSObject { } func removeMediaChangeObserver() { - playerObservations = playerObservations.filter { obs in - obs.invalidate() - return false + if !playerObservations.isEmpty { + let mediaChangeObserver = playerObservations.removeFirst() + mediaChangeObserver.invalidate() } } @@ -159,6 +189,7 @@ public class SMPlayerListeners: NSObject { func removePlayerObservers() { playerObservations.forEach { $0.invalidate() } playerObservations.removeAll() + removePeriodicTimeObserver() removeItemObservers() } From 9250e09c399fcdfbb63cd921266f97547c1a4576 Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 4 Jun 2025 13:14:41 -0300 Subject: [PATCH 31/34] added deinit where necessary --- .../ios/Classes/NotificationManager.swift | 12 +++++++++++ .../ios/Classes/NowPlayingInfoManager.swift | 4 ++++ .../player/ios/Classes/QueueManager.swift | 4 ++++ packages/player/ios/Classes/SMPlayer.swift | 20 ++++++++++++------- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/player/ios/Classes/NotificationManager.swift b/packages/player/ios/Classes/NotificationManager.swift index e4902b80..1d9461d1 100644 --- a/packages/player/ios/Classes/NotificationManager.swift +++ b/packages/player/ios/Classes/NotificationManager.swift @@ -37,4 +37,16 @@ class NotificationManager { 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 index 4f0912ec..ca60ed2e 100644 --- a/packages/player/ios/Classes/NowPlayingInfoManager.swift +++ b/packages/player/ios/Classes/NowPlayingInfoManager.swift @@ -68,4 +68,8 @@ class NowPlayingInfoManager { MPNowPlayingInfoCenter.default().nowPlayingInfo = nil removeNotification() } + + deinit { + removeNotification() + } } diff --git a/packages/player/ios/Classes/QueueManager.swift b/packages/player/ios/Classes/QueueManager.swift index 5c1c7adb..6f30f200 100644 --- a/packages/player/ios/Classes/QueueManager.swift +++ b/packages/player/ios/Classes/QueueManager.swift @@ -293,4 +293,8 @@ class QueueManager { return createLocalPlayerItem(with: uri!) } } + + deinit { + removeAll() + } } diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 2494be54..be2f6d71 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -136,7 +136,7 @@ public class SMPlayer : NSObject { let isFirstBatch = self.smPlayer.items().count == 0 for media in medias { media.cookie = cookie - queueManager.enqueue(item: media) + queueManager.enqueue(item: media) } queueManager.insertIntoPlayerIfNeeded() if autoPlay && isFirstBatch { @@ -171,7 +171,6 @@ public class SMPlayer : NSObject { nowPlayingInfoManager.enableCommands() } - //override automatic next @objc func itemDidFinishPlaying(_ notification: Notification) { pause() switch smPlayer.repeatMode { @@ -189,10 +188,6 @@ public class SMPlayer : NSObject { smPlayer.play() } - private func removeAllObservers() { - notificationManager.removeAllObservers() - } - private func notifyMediaChangedIfNeeded() { let isNotFirstItem = smPlayer.currentItem != fullQueue.first let hasHistory = queueManager.historyQueue.count > 0 @@ -201,6 +196,18 @@ public class SMPlayer : NSObject { methodChannelManager?.notifyPlayerStateChange(state: PlayerState.itemTransition) } } + + deinit { + removeEndPlaybackObserver() + notificationManager.removeAudioInterruptionObserver() + listeners?.removePlayerObservers() + listeners = nil + queueManager.removeAll() + nowPlayingInfoManager.removeNotification() + clearNowPlayingInfo() + smPlayer.pause() + smPlayer.removeAllItems() + } } extension AVPlayerItem { @@ -244,5 +251,4 @@ public extension AVQueuePlayer { var repeatModeIndex: Int { return RepeatMode.allCases.firstIndex(of: repeatMode) ?? -1 } - } From 10d7f2d25bf0e983d06aa6fbb7b2f1d3be1cba9c Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 4 Jun 2025 16:06:22 -0300 Subject: [PATCH 32/34] improve code --- .../player/ios/Classes/PlayerPlugin.swift | 2 +- .../player/ios/Classes/QueueManager.swift | 204 ++++++++-------- packages/player/ios/Classes/SMPlayer.swift | 13 +- .../ios/Classes/SMPlayerListeners.swift | 218 +++++++++--------- 4 files changed, 216 insertions(+), 221 deletions(-) diff --git a/packages/player/ios/Classes/PlayerPlugin.swift b/packages/player/ios/Classes/PlayerPlugin.swift index 0de18345..4e256de6 100644 --- a/packages/player/ios/Classes/PlayerPlugin.swift +++ b/packages/player/ios/Classes/PlayerPlugin.swift @@ -34,7 +34,7 @@ public class PlayerPlugin: NSObject, FlutterPlugin { } result(NSNumber(value: true)) case "next": - smPlayer?.queueManager.nextTrack(from:"next call") + smPlayer?.queueManager.nextTrack() result(NSNumber(value: 1)) case "disable_notification_commands": smPlayer?.areNotificationCommandsEnabled = false diff --git a/packages/player/ios/Classes/QueueManager.swift b/packages/player/ios/Classes/QueueManager.swift index 6f30f200..bb38b771 100644 --- a/packages/player/ios/Classes/QueueManager.swift +++ b/packages/player/ios/Classes/QueueManager.swift @@ -1,15 +1,14 @@ import AVFoundation class QueueManager { - var historyQueue: [PlaylistItem] = [] - var futureQueue: [PlaylistItem] = [] - var originalQueue: [PlaylistItem] = [] - var shuffledQueue: [PlaylistItem] = [] + var historyQueue = [PlaylistItem]() + var futureQueue = [PlaylistItem]() + var originalQueue = [PlaylistItem]() + var shuffledQueue = [PlaylistItem]() - var shuffledIndices: [Int] = [] - var isShuffleModeEnabled: Bool = false + var shuffledIndices = [Int]() + var isShuffleModeEnabled = false private let smPlayer: AVQueuePlayer - private let listeners: SMPlayerListeners private let methodChannelManager: MethodChannelManager @@ -24,37 +23,32 @@ class QueueManager { self.methodChannelManager = methodChannelManager } - var mirrorPlayerQueue: [PlaylistItem] { - return smPlayer.items().compactMap { $0.playlistItem} + smPlayer.items().compactMap { $0.playlistItem } } var fullQueue: [PlaylistItem] { - return historyQueue + mirrorPlayerQueue + futureQueue + historyQueue + mirrorPlayerQueue + futureQueue } - func seekToTimePosition(position:Int){ + func seekToTimePosition(position: Int) { let positionInSec = CMTime(seconds: Double(position/1000), preferredTimescale: Constants.defaultTimescale) - smPlayer.currentItem?.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { completed in + smPlayer.currentItem?.seek(to: positionInSec, toleranceBefore: .zero, toleranceAfter: .zero) { completed in if completed { self.methodChannelManager.notifyPlayerStateChange(state: PlayerState.seekEnd) } - } ) + } + smPlayer.play() } var currentIndex: Int { - guard let currentItem = smPlayer.currentItem?.playlistItem else { - return 0 - } + guard let currentItem = smPlayer.currentItem?.playlistItem else { return 0 } return fullQueue.firstIndex(of: currentItem) ?? 0 } func fillShuffledQueue() { - shuffledQueue.removeAll() - for index in shuffledIndices { - if index < fullQueue.count { - shuffledQueue.append(fullQueue[index]) - } + shuffledQueue = shuffledIndices.compactMap { index in + index < fullQueue.count ? fullQueue[index] : nil } } @@ -65,16 +59,16 @@ class QueueManager { } 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) + 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") } @@ -85,25 +79,21 @@ class QueueManager { originalQueue = fullQueue fillShuffledQueue() distributeItemsInRightQueue(currentQueue: shuffledQueue) - } else { - if !originalQueue.isEmpty { - distributeItemsInRightQueue(currentQueue: originalQueue) - } + } else if !originalQueue.isEmpty { + distributeItemsInRightQueue(currentQueue: originalQueue) } } func distributeItemsInRightQueue(currentQueue: [PlaylistItem], keepFirst: Bool = true, positionArg: Int = -1, completionHandler completion: (() -> Void)? = nil) { - guard currentQueue.count > 0 else { return } + guard !currentQueue.isEmpty else { return } + var position = positionArg historyQueue.removeAll() futureQueue.removeAll() if keepFirst { - position = (smPlayer.currentItem?.playlistItem != nil ? currentQueue.firstIndex(of: smPlayer.currentItem!.playlistItem!) : -1)! - let itemsToRemove = smPlayer.items().dropFirst() - for item in itemsToRemove { - smPlayer.remove(item) - } + 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 { @@ -111,36 +101,39 @@ class QueueManager { } } - for (index, item) in currentQueue.enumerated() { - if index != position { - if index < position { - historyQueue.append(item) - } else { - futureQueue.append(item) - } + 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.. \(String(describing: smPlayer.currentItem?.playlistItem?.title))") - printStatus(from:"insertIntoPlayerIfNeeded") + + Logger.debugLog("#NATIVE LOGS insertIntoPlayerIfNeeded ==> \(smPlayer.currentItem?.playlistItem?.title ?? "Unknown")") + printStatus(from: "insertIntoPlayerIfNeeded") } func removeAll() { smPlayer.pause() - seekToTimePosition(position:0) + seekToTimePosition(position: 0) smPlayer.removeAllItems() historyQueue.removeAll() futureQueue.removeAll() @@ -150,10 +143,12 @@ class QueueManager { } func updateMediaUri(id: Int, uri: String?) { - guard let newUri = uri else { return } - guard let index = fullQueue.firstIndex(where: { $0.mediaId == id }) else { return } + guard let newUri = uri, + let index = fullQueue.firstIndex(where: { $0.mediaId == id }) else { return } + let oldItem = fullQueue[index] - if oldItem.url == newUri { return } + guard oldItem.url != newUri else { return } + let updatedItem = createUpdatedPlaylistItem(from: oldItem, newUri: newUri) var updatedQueue = fullQueue updatedQueue[index] = updatedItem @@ -161,7 +156,7 @@ class QueueManager { } private func createUpdatedPlaylistItem(from oldItem: PlaylistItem, newUri: String) -> PlaylistItem { - return PlaylistItem( + PlaylistItem( albumId: oldItem.albumId, albumName: oldItem.albumName, title: oldItem.title, @@ -176,99 +171,99 @@ class QueueManager { } func playFromQueue(position: Int, timePosition: Int = 0, loadOnly: Bool = false) { - distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position, completionHandler: { + distributeItemsInRightQueue(currentQueue: fullQueue, keepFirst: false, positionArg: position) { self.methodChannelManager.currentMediaIndex(index: self.currentIndex) if timePosition > 0 { - self.seekToTimePosition(position:timePosition) + self.seekToTimePosition(position: timePosition) } - }) + } + if loadOnly { smPlayer.pause() -// shouldNotifyTransition = false } else { smPlayer.play() } } - func nextTrack(from: String) { + func nextTrack() { smPlayer.pause() - if let currentItem = smPlayer.currentItem?.playlistItem{ + + if let currentItem = smPlayer.currentItem?.playlistItem { historyQueue.append(currentItem) } + if smPlayer.currentItem?.playlistItem == fullQueue.last && smPlayer.repeatMode == .REPEAT_MODE_ALL { - playFromQueue(position:0) + playFromQueue(position: 0) + return } + smPlayer.advanceToNextItem() - seekToTimePosition(position:0) + seekToTimePosition(position: 0) insertIntoPlayerIfNeeded() smPlayer.play() } func previousTrack() { smPlayer.pause() + guard let lastHistoryItem = historyQueue.popLast() else { - seekToTimePosition(position:0) + seekToTimePosition(position: 0) smPlayer.play() return } - guard let currentItem = smPlayer.currentItem else { - return - } - - guard let lastItemInPlayer = smPlayer.items().last else { - return - } + guard let currentItem = smPlayer.currentItem, + let lastItemInPlayer = smPlayer.items().last else { return } if currentItem != lastItemInPlayer { smPlayer.remove(lastItemInPlayer) - futureQueue.insert(lastItemInPlayer.playlistItem!, at: 0) + if let playlistItem = lastItemInPlayer.playlistItem { + futureQueue.insert(playlistItem, at: 0) + } } - let historyAVPlayerItem = createPlayerItemFromUri(lastHistoryItem.url, fallbackUrl:lastHistoryItem.fallbackUrl,cookie:lastHistoryItem.cookie) + guard let historyAVPlayerItem = createPlayerItemFromUri(lastHistoryItem.url, fallbackUrl: lastHistoryItem.fallbackUrl, cookie: lastHistoryItem.cookie) else { return } - if historyAVPlayerItem != nil { - historyAVPlayerItem!.playlistItem = lastHistoryItem - smPlayer.insert(historyAVPlayerItem!, after: currentItem) - smPlayer.advanceToNextItem() - smPlayer.insert(currentItem, after: smPlayer.currentItem) - } + historyAVPlayerItem.playlistItem = lastHistoryItem + smPlayer.insert(historyAVPlayerItem, after: currentItem) + smPlayer.advanceToNextItem() + smPlayer.insert(currentItem, after: smPlayer.currentItem) - seekToTimePosition(position:0) + seekToTimePosition(position: 0) insertIntoPlayerIfNeeded() listeners.addItemsObservers() smPlayer.play() } func getCurrentPlaylistItem() -> PlaylistItem? { - guard let currentItem = smPlayer.currentItem else { - return nil - } - return currentItem.playlistItem + smPlayer.currentItem?.playlistItem } func printStatus(from: String) { Logger.debugLog("QueueActivity #################################################") Logger.debugLog("QueueActivity \(from) ") - Logger.debugLog("QueueActivity Current Index: \(String(describing: currentIndex))") + Logger.debugLog("QueueActivity Current Index: \(currentIndex)") Logger.debugLog("QueueActivity ------------------------------------------") Logger.debugLog("QueueActivity printStatus History: \(historyQueue.count) items") - for item in historyQueue { - Logger.debugLog("QueueActivity printStatus History: \(String(describing: item.title))") + historyQueue.forEach { item in + Logger.debugLog("QueueActivity printStatus History: \(item.title)") } + Logger.debugLog("QueueActivity printStatus ------------------------------------------") Logger.debugLog("QueueActivity printStatus futureQueue Items: \(futureQueue.count) items") - for item in futureQueue { - Logger.debugLog("QueueActivity printStatus Upcoming: \(String(describing: item.title))") + futureQueue.forEach { item in + Logger.debugLog("QueueActivity printStatus Upcoming: \(item.title)") } + Logger.debugLog("QueueActivity printStatus ------------------------------------------") Logger.debugLog("QueueActivity printStatus AVQueuePlayer items: \(smPlayer.items().count)") - for item in smPlayer.items() { - Logger.debugLog("QueueActivity printStatus AVQueuePlayer: \(String(describing: item.playlistItem?.title))") + smPlayer.items().forEach { item in + Logger.debugLog("QueueActivity printStatus AVQueuePlayer: \(item.playlistItem?.title ?? "Unknown")") } + Logger.debugLog("QueueActivity printStatus #################################################") } @@ -276,21 +271,16 @@ class QueueManager { futureQueue.append(item) } - private func createPlayerItem(with url: URL, cookie: String?) -> AVPlayerItem { - let assetOptions = ["AVURLAssetHTTPHeaderFieldsKey": ["Cookie": cookie ?? ""]] - return AVPlayerItem(asset: AVURLAsset(url: url, options: assetOptions)) - } - - private func createLocalPlayerItem(with path: String) -> AVPlayerItem { - return AVPlayerItem(asset: AVAsset(url: NSURL(fileURLWithPath: path) as URL)) - } - func createPlayerItemFromUri(_ uri: String?, fallbackUrl: String?, cookie: String?) -> AVPlayerItem? { - if uri?.contains("https") ?? true { - guard let url = URL(string: (uri ?? fallbackUrl) ?? "") else { return nil } - return createPlayerItem(with: url, cookie: cookie) + 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 createLocalPlayerItem(with: uri!) + return AVPlayerItem(asset: AVAsset(url: URL(fileURLWithPath: urlString))) } } diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index be2f6d71..58908d9d 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -39,14 +39,11 @@ public class SMPlayer : NSObject { 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) @@ -55,7 +52,7 @@ public class SMPlayer : NSObject { 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(from: "commandCenter.nextTrackCommand") }, + nextTrack: { [weak self] in self?.queueManager.nextTrack() }, previousTrack: { [weak self] in self?.queueManager.previousTrack() }, seekToPosition: { [weak self] pos in self?.queueManager.seekToTimePosition(position: pos) } ) @@ -70,7 +67,7 @@ public class SMPlayer : NSObject { } switch type { case .began: - pause() + smPlayer.pause() case .ended: if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt { let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) @@ -172,18 +169,18 @@ public class SMPlayer : NSObject { } @objc func itemDidFinishPlaying(_ notification: Notification) { - pause() + smPlayer.pause() switch smPlayer.repeatMode { case .REPEAT_MODE_ALL: if(smPlayer.currentItem == fullQueue.last){ queueManager.playFromQueue(position: 0) break } - queueManager.nextTrack(from:"REPEAT_MODE_ALL") + queueManager.nextTrack() case .REPEAT_MODE_ONE: queueManager.seekToTimePosition(position: 0) case .REPEAT_MODE_OFF: - queueManager.nextTrack(from: "REPEAT_MODE_OFF") + queueManager.nextTrack() } smPlayer.play() } diff --git a/packages/player/ios/Classes/SMPlayerListeners.swift b/packages/player/ios/Classes/SMPlayerListeners.swift index fb4c71be..57f48ae9 100644 --- a/packages/player/ios/Classes/SMPlayerListeners.swift +++ b/packages/player/ios/Classes/SMPlayerListeners.swift @@ -1,157 +1,175 @@ import Foundation import AVFoundation -private enum ObserverKey: String { - case status, isPlaybackBufferEmpty, isPlaybackLikelyToKeepUp, currentItem, reasonForWaitingToPlay, timeControlStatus -} - public class SMPlayerListeners: NSObject { let smPlayer: AVQueuePlayer weak var methodChannelManager: MethodChannelManager? var onMediaChanged: ((Bool) -> Void)? - private var itemObservations = [NSKeyValueObservation]() - private var playerObservations = [NSKeyValueObservation]() + 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() } func addItemsObservers() { - removeItemObservers() + cleanupItemObservers() guard let currentItem = smPlayer.currentItem else { return } - let statusObs = currentItem.observe(\AVPlayerItem.status, options: [.new, .old]) { [weak self] playerItem, _ in - guard let self = self else { return } + let statusObservation = currentItem.observe( + \AVPlayerItem.status, + options: [.new, .initial] + ) { [weak self] playerItem, _ in switch playerItem.status { case .failed: - if let error = playerItem.error { - Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] ERROR: \(error.localizedDescription)") - self.methodChannelManager?.notifyError(error: error.localizedDescription) - } else { - self.methodChannelManager?.notifyError(error: "Unknown error") - } + 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: PlayerState.stateReady) + self?.notifyPlayerStateChange(state: .stateReady) case .unknown: - break + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Player status unknown") @unknown default: - break + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Player status unknown default") } } - itemObservations.append(statusObs) + itemObservations.insert(statusObservation) - let loadingObs = currentItem.observe(\AVPlayerItem.isPlaybackBufferEmpty, options: [.new, .old]) { [weak self] _, _ in - guard let self = self else { return } - Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Buffering (isPlaybackBufferEmpty)") - self.notifyPlayerStateChange(state: PlayerState.buffering) + 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.append(loadingObs) + itemObservations.insert(bufferEmptyObservation) - let loadedObs = currentItem.observe(\AVPlayerItem.isPlaybackLikelyToKeepUp, options: [.new]) { [weak self] _, _ in - guard self != nil else { return } - Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Loaded (isPlaybackLikelyToKeepUp)") + 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.append(loadedObs) + itemObservations.insert(bufferKeepUpObservation) - addErrorObserver(for: currentItem) - } - - private func addErrorObserver(for item: AVPlayerItem) { - NotificationCenter.default.addObserver( - self, - selector: #selector(handlePlayerItemError(_:)), - name: .AVPlayerItemFailedToPlayToEndTime, - object: item - ) - } - - @objc private func handlePlayerItemError(_ notification: Notification) { - if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error { - Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Player Item Error: \(error.localizedDescription)") - methodChannelManager?.notifyError(error: error.localizedDescription) + 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 notifyPlayerStateChange(state: PlayerState) { - if lastState != state { - methodChannelManager?.notifyPlayerStateChange(state: state) - lastState = state + let now = Date() + guard lastState != state && (now.timeIntervalSince(lastNotificationTime) >= notificationThrottleInterval || lastState != state) else { + return } + + lastNotificationTime = now + methodChannelManager?.notifyPlayerStateChange(state: state) + lastState = state } func addMediaChangeObserver() { - removeMediaChangeObserver() - let mediaChangeObs = smPlayer.observe(\AVQueuePlayer.currentItem, options: [.new, .old]) { [weak self] _, change in - guard let self = self else { return } - let oldItemExists = change.oldValue != nil - Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Media changed. Old item exists: \(oldItemExists)") + let mediaChangeObservation = smPlayer.observe( + \AVQueuePlayer.currentItem, + options: [.new, .old] + ) { [weak self] _, change in if let newItem = change.newValue, newItem != change.oldValue { - self.onMediaChanged?(true) - 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.append(mediaChangeObs) + playerObservations.insert(mediaChangeObservation) } func addPlayerObservers() { addMediaChangeObserver() addPeriodicTimeObserver() - let notPlayingReasonObs = smPlayer.observe(\AVQueuePlayer.reasonForWaitingToPlay, options: [.new]) { [weak self] player, _ in - guard self != nil else { return } - switch player.reasonForWaitingToPlay { - case .evaluatingBufferingRate: - Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Reason: evaluatingBufferingRate") - case .toMinimizeStalls: - Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Reason: toMinimizeStalls") - case .noItemToPlay: - Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Reason: noItemToPlay") - default: - Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Reason: 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.append(notPlayingReasonObs) + playerObservations.insert(reasonObservation) - let playbackObs = smPlayer.observe(\AVQueuePlayer.timeControlStatus, options: [.new, .old]) { [weak self] player, _ 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.notifyPlayerStateChange(state: PlayerState.playing) + self?.notifyPlayerStateChange(state: .playing) Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Playing") case .paused: - self.notifyPlayerStateChange(state: PlayerState.paused) + self?.notifyPlayerStateChange(state: .paused) Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Paused") case .waitingToPlayAtSpecifiedRate: - self.notifyPlayerStateChange(state: PlayerState.buffering) + self?.notifyPlayerStateChange(state: .buffering) Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Buffering") @unknown default: - break + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Unknown time control status") } } - playerObservations.append(playbackObs) + playerObservations.insert(playbackObservation) } private func addPeriodicTimeObserver() { removePeriodicTimeObserver() - let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - periodicTimeObserver = smPlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ 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) - if let playlistItem = currentItem.playlistItem { - NowPlayingCenter.update(item: playlistItem, rate: 1.0, position: position, duration: duration) - } - } - } + + 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 + } + + methodChannelManager?.notifyPositionChange(position: position, duration: duration) + + if let playlistItem = currentItem.playlistItem { + NowPlayingCenter.update(item: playlistItem, rate: 1.0, position: position, duration: duration) } } @@ -162,39 +180,29 @@ public class SMPlayerListeners: NSObject { } } - func removeItemObservers() { + private func cleanupItemObservers() { itemObservations.forEach { $0.invalidate() } itemObservations.removeAll() - removeErrorObserver() - } - - func removeMediaChangeObserver() { - if !playerObservations.isEmpty { - let mediaChangeObserver = playerObservations.removeFirst() - mediaChangeObserver.invalidate() - } + cleanupNotificationObservers() } - func removeErrorObserver() { - if let currentItem = smPlayer.currentItem { - NotificationCenter.default.removeObserver( - self, - name: .AVPlayerItemFailedToPlayToEndTime, - object: currentItem - ) + private func cleanupNotificationObservers() { + notificationObservers.forEach { observer in + NotificationCenter.default.removeObserver(observer) } + notificationObservers.removeAll() } - /// Removes all player observers. func removePlayerObservers() { playerObservations.forEach { $0.invalidate() } playerObservations.removeAll() removePeriodicTimeObserver() - removeItemObservers() + cleanupItemObservers() } deinit { removePlayerObservers() + Logger.debugLog("#NATIVE LOGS ==> [SMPlayerListeners] Deinitializing") } } From 3a1a834698ed064959e955db0ea94d6b2890b92f Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 4 Jun 2025 18:45:41 -0300 Subject: [PATCH 33/34] remove play when seek --- packages/player/ios/Classes/QueueManager.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/player/ios/Classes/QueueManager.swift b/packages/player/ios/Classes/QueueManager.swift index bb38b771..636a7340 100644 --- a/packages/player/ios/Classes/QueueManager.swift +++ b/packages/player/ios/Classes/QueueManager.swift @@ -38,7 +38,6 @@ class QueueManager { self.methodChannelManager.notifyPlayerStateChange(state: PlayerState.seekEnd) } } - smPlayer.play() } var currentIndex: Int { From 63f326d7bb6946f192d1ed61173ae93aab6364bd Mon Sep 17 00:00:00 2001 From: lstonussi Date: Wed, 4 Jun 2025 19:11:08 -0300 Subject: [PATCH 34/34] use right cookie variable --- packages/player/ios/Classes/SMPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/player/ios/Classes/SMPlayer.swift b/packages/player/ios/Classes/SMPlayer.swift index 58908d9d..f3e36fb4 100644 --- a/packages/player/ios/Classes/SMPlayer.swift +++ b/packages/player/ios/Classes/SMPlayer.swift @@ -132,7 +132,7 @@ public class SMPlayer : NSObject { } let isFirstBatch = self.smPlayer.items().count == 0 for media in medias { - media.cookie = cookie + media.cookie = self.cookie queueManager.enqueue(item: media) } queueManager.insertIntoPlayerIfNeeded()