Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1fa075c
added interruptionObserver
Dec 18, 2024
37013f9
cast working v1
Dec 20, 2024
9b553fc
example and others files
Dec 20, 2024
cf904d4
controls ok
Dec 23, 2024
ac52aaf
remove empty spaces
Dec 23, 2024
7ff585a
working
Dec 24, 2024
6f6e1b5
fix pause when buffering and wrap player
Jan 14, 2025
a0cf2ce
added finished event
Jan 15, 2025
e3f2193
improve logs
Jan 15, 2025
ee758a7
fix seek behavior when rewind
Jan 15, 2025
701fb5b
set shouldNotifyTransition false to default
Jan 16, 2025
2f521d1
fix exemple and use events from player
Jan 17, 2025
f2f47a7
ready to cast
Jan 20, 2025
295c90e
remove shouldTransition argument
Jan 21, 2025
3dd1936
workaround to use queue in cast
Jan 23, 2025
127728c
fix shuffle
Jan 23, 2025
ed1fa15
wip
May 19, 2025
09e5bad
wip
May 27, 2025
1873b1e
improves and reorganizing
May 28, 2025
07cb25f
remove unused methods
May 28, 2025
3703b26
Enhance error handling in PlayerSwitcher and improve Queue initializa…
May 29, 2025
b75ae05
improve folders
May 29, 2025
e7ac240
Refactor event and player state management by moving enums to a dedic…
May 29, 2025
130505d
Refactor MediaService by encapsulating player variables and ensuring …
May 29, 2025
6920f13
fix channel
May 31, 2025
30d2268
Add Logger, NotificationManager, NowPlayingInfoManager, and QueueMana…
Jun 2, 2025
f7edd69
improve listeners
Jun 2, 2025
970dbec
improve performance
Jun 3, 2025
ffa77a1
remove unecessary stuffs
Jun 3, 2025
26f4a3a
fix observers
Jun 4, 2025
9250e09
added deinit where necessary
Jun 4, 2025
10d7f2d
improve code
Jun 4, 2025
3a1a834
remove play when seek
Jun 4, 2025
63f326d
use right cookie variable
Jun 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions packages/player/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -32,7 +32,7 @@ kapt {
}

android {
compileSdkVersion 34
compileSdk = 35

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
Expand All @@ -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
Expand All @@ -60,8 +60,11 @@ dependencies {
implementation "androidx.media3:media3-ui:$media3_version"

// implementation files('/Users/lucastonussi/flutter/bin/cache/artifacts/engine/android-x64/flutter.jar')
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1'

implementation "com.google.code.gson:gson:2.10.1"

// api 'com.google.android.gms:play-services-cast-framework:22.0.0'
// CHROMECAST DEPENDENCIES
implementation 'androidx.mediarouter:mediarouter:1.7.0'
implementation 'androidx.media3:media3-cast:1.5.1'
}
3 changes: 3 additions & 0 deletions packages/player/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<application>
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="br.com.suamusica.player.CastOptionsProvider" />
<service android:name=".MediaService" android:exported="false" android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
package br.com.suamusica.player

import android.content.Context
import android.util.Log
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.util.UnstableApi
import androidx.mediarouter.media.MediaControlIntent
import androidx.mediarouter.media.MediaControlIntent.CATEGORY_LIVE_AUDIO
import androidx.mediarouter.media.MediaControlIntent.CATEGORY_LIVE_VIDEO
import androidx.mediarouter.media.MediaControlIntent.CATEGORY_REMOTE_PLAYBACK
import androidx.mediarouter.media.MediaRouteSelector
import androidx.mediarouter.media.MediaRouter
import androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_DISCONNECTED
import androidx.mediarouter.media.RemotePlaybackClient
import com.google.android.gms.cast.*
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.cast.framework.CastStateListener
import com.google.android.gms.cast.framework.Session
import com.google.android.gms.cast.framework.SessionManager
import com.google.android.gms.cast.framework.SessionManagerListener
import com.google.android.gms.common.api.PendingResult
import com.google.android.gms.common.api.Status


@UnstableApi

class CastManager(
castContext: CastContext,
context: Context,
) :
SessionAvailabilityListener,
CastStateListener,
Cast.Listener(),
SessionManagerListener<Session>,
PendingResult.StatusListener {
companion object {
const val TAG = "Chromecast"
}

private var mediaRouter = MediaRouter.getInstance(context)
var isConnected = false
private var sessionManager: SessionManager? = null
private var mediaRouterCallback: MediaRouter.Callback? = null
private var onConnectCallback: (() -> Unit)? = null
private var onSessionEndedCallback: (() -> Unit)? = null
private var alreadyConnected = false
private var cookie: String = ""

init {
castContext.addCastStateListener(this)
sessionManager = castContext.sessionManager

//TODO: pode remover esse callback?
mediaRouterCallback = object : MediaRouter.Callback() {
override fun onRouteAdded(router: MediaRouter, route: MediaRouter.RouteInfo) {
super.onRouteAdded(router, route)
Log.d(TAG, "#NATIVE LOGS CAST ==> Route added: " + route.getName())
}

override fun onRouteRemoved(router: MediaRouter, route: MediaRouter.RouteInfo) {
super.onRouteRemoved(router, route)
Log.d(TAG, "#NATIVE LOGS CAST ==> Route removed: " + route.getName())
}

override fun onRouteChanged(router: MediaRouter, route: MediaRouter.RouteInfo) {
super.onRouteChanged(router, route)
Log.d(TAG, "#NATIVE LOGS CAST ==> Route changed: " + route.getName())
}

override fun onRouteSelected(
router: MediaRouter,
route: MediaRouter.RouteInfo,
reason: Int
) {
Log.d(
TAG,
"#NATIVE LOGS CAST ==> Route selected: " + route.getName() + ", reason: " + reason
)
}

override fun onRouteUnselected(
router: MediaRouter,
route: MediaRouter.RouteInfo,
reason: Int
) {
Log.d(
TAG,
"#NATIVE LOGS CAST ==> Route unselected: " + route.getName() + ", reason: " + reason
)
}
}

val selector: MediaRouteSelector.Builder = MediaRouteSelector.Builder()
.addControlCategory(CATEGORY_REMOTE_PLAYBACK)
//TODO: remover?
mediaRouterCallback?.let {
mediaRouter.addCallback(
selector.build(), it,
MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
)
}
}

// fun discoveryCast(): List<Map<String, String>> {
// val casts = mutableListOf<Map<String, String>>()
// if (castContext.castState != CastState.NO_DEVICES_AVAILABLE) {
// mediaRouter.routes.forEach {
// if (it.deviceType == DEVICE_TYPE_TV && it.id.isNotEmpty()) {
// casts.add(
// mapOf(
// "name" to it.name,
// "id" to it.id,
// )
// )
// }
// }
// }
// return casts
// }

fun connectToCast(idCast: String) {
val item = mediaRouter.routes.firstOrNull {
it.id.contains(idCast)
}
if (!isConnected) {
if (item != null) {
mediaRouter.selectRoute(item)
return
}
} else {
mediaRouter.unselect(UNSELECT_REASON_DISCONNECTED)
}
}

fun disconnect() {
if (isConnected) {
sessionManager?.endCurrentSession(true)
onSessionEndedCallback?.invoke()
isConnected = false
}
}

// private fun createQueueItem(mediaItem: MediaItem): MediaQueueItem {
// val mediaInfo = createMediaInfo(mediaItem)
// return MediaQueueItem.Builder(mediaInfo).build()
// }
//
// fun queueLoadCast(mediaItems: List<MediaItem>) {
// val mediaQueueItems = mediaItems.map { mediaItem ->
// createQueueItem(mediaItem)
// }
//
// val cookieOk = cookie.replace("CloudFront-Policy=", "{\"CloudFront-Policy\": \"")
// .replace(";CloudFront-Key-Pair-Id=", "\", \"CloudFront-Key-Pair-Id\": \"")
// .replace(";CloudFront-Signature=", "\", \"CloudFront-Signature\": \"") + "\"}"
//
//
// val credentials = JSONObject().put("credentials", cookieOk)
//
//
// val request = sessionManager?.currentCastSession?.remoteMediaClient?.queueLoad(
// mediaQueueItems.toTypedArray(),
// player!!.currentMediaItemIndex,
// 1,
// player.currentPosition,
// credentials,
// )
//
// request?.addStatusListener(this)
// }

// fun loadMediaOld() {
// val media = player!!.currentMediaItem
// val url = media?.associatedMedia?.coverUrl!!
//
// val musictrackMetaData = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK)
// musictrackMetaData.putString(MediaMetadata.KEY_TITLE, media.associatedMedia?.name!!)
// musictrackMetaData.putString(MediaMetadata.KEY_ARTIST, media.associatedMedia?.author!!)
// musictrackMetaData.putString(MediaMetadata.KEY_ALBUM_TITLE, "albumName")
// musictrackMetaData.putString("images", url)
//
// media.associatedMedia?.coverUrl?.let {
// musictrackMetaData.addImage(WebImage(Uri.parse(it)))
// }
//
// val mediaInfo =
// MediaInfo.Builder(media.associatedMedia?.url!!)
// .setContentUrl(media.associatedMedia?.url!!)
// .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
// .setMetadata(musictrackMetaData)
// .build()
//
// val cookieOk = cookie.replace("CloudFront-Policy=", "{\"CloudFront-Policy\": \"")
// .replace(";CloudFront-Key-Pair-Id=", "\", \"CloudFront-Key-Pair-Id\": \"")
// .replace(";CloudFront-Signature=", "\", \"CloudFront-Signature\": \"") + "\"}"
//
// val options = MediaLoadOptions.Builder()
//// .setPlayPosition(player.currentPosition)
// .setCredentials(
// cookieOk
// )
// .build()
//
// val request =
// sessionManager?.currentCastSession?.remoteMediaClient?.load(mediaInfo, options)
// request?.addStatusListener(this)
// }

// val remoteMediaClient: RemoteMediaClient?
// get() = sessionManager?.currentCastSession?.remoteMediaClient


// private fun createMediaInfo(mediaItem: MediaItem): MediaInfo {
// val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK).apply {
// putString(MediaMetadata.KEY_TITLE, mediaItem.associatedMedia?.name ?: "Title")
// putString(MediaMetadata.KEY_ARTIST, mediaItem.associatedMedia?.author ?: "Artist")
// putString(
// MediaMetadata.KEY_ALBUM_TITLE,
// mediaItem.associatedMedia?.albumTitle ?: "Album"
// )
//
// mediaItem.associatedMedia?.coverUrl?.let {
// putString("images", it)
// }
// mediaItem.associatedMedia?.coverUrl?.let { coverUrl ->
// try {
// addImage(WebImage(Uri.parse(coverUrl.trim())))
// } catch (e: Exception) {
// Log.e(TAG, "Failed to add cover image: ${e.message}")
// }
// }
// }
//
// return MediaInfo.Builder(mediaItem.associatedMedia?.url!!)
// .setContentUrl(mediaItem.associatedMedia?.url!!)
// .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
// .setMetadata(metadata)
// .build()
// }

//CAST STATE LISTENER
override fun onCastStateChanged(state: Int) {
Log.d(
TAG,
"#NATIVE LOGS CAST ==> RECEIVER UPDATE AVAILABLE ${CastState.toString(state)}"
)

if (alreadyConnected && state == CastState.NOT_CONNECTED) {
alreadyConnected = false
}

if (!alreadyConnected) {
isConnected = state == CastState.CONNECTED
if (isConnected) {
alreadyConnected = true
onConnectCallback?.invoke()
}
}
}

//SessionAvailabilityListener
override fun onCastSessionAvailable() {
Log.d(TAG, "#NATIVE LOGS CAST ==>- SessionAvailabilityListener: onCastSessionAvailable")
}

override fun onCastSessionUnavailable() {
Log.d(TAG, "#NATIVE LOGS CAST ==>- SessionAvailabilityListener: onCastSessionUnavailable")
}

//PendingResult.StatusListener
override fun onComplete(status: Status) {
Log.d(TAG, "#NATIVE LOGS CAST ==> onComplete $status")
}


//SESSION MANAGER LISTENER
override fun onSessionEnded(p0: Session, p1: Int) {
Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionEnded")
}

override fun onSessionEnding(p0: Session) {
Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionEnding")
}

override fun onSessionResumeFailed(p0: Session, p1: Int) {
Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResumeFailed")
}

override fun onSessionResumed(p0: Session, p1: Boolean) {
Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResumed")
}

override fun onSessionResuming(p0: Session, p1: String) {
Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionResuming")
}

override fun onSessionStartFailed(p0: Session, p1: Int) {
Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionStartFailed $p0, $p1")
}

override fun onSessionStarted(p0: Session, p1: String) {
Log.d(TAG, "#NATIVE LOGS CAST ==> onCastSessionUnavailable")
onSessionEndedCallback?.invoke()
}

override fun onSessionStarting(p0: Session) {
Log.d(TAG, "#NATIVE LOGS CAST ==> $p0 onSessionStarting")
// OnePlayerSingleton.toggleCurrentPlayer(true)
}

override fun onSessionSuspended(p0: Session, p1: Int) {
Log.d(TAG, "#NATIVE LOGS CAST ==> onSessionSuspended")
}

// var MediaItem.associatedMedia: Media?
// get() = mediaItemMediaAssociations[this]
// set(value) {
// mediaItemMediaAssociations[this] = value
// }

fun setOnConnectCallback(callback: () -> Unit) {
onConnectCallback = callback
}

fun setOnSessionEndedCallback(callback: () -> Unit) {
onSessionEndedCallback = callback
}
}
Loading