Start of chromecast support
This commit is contained in:
parent
978aed5498
commit
0a666c49c5
18 changed files with 241 additions and 5 deletions
|
@ -106,6 +106,8 @@ dependencies {
|
||||||
implementation(libs.jellyfin.core)
|
implementation(libs.jellyfin.core)
|
||||||
compileOnly(libs.libmpv)
|
compileOnly(libs.libmpv)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
|
implementation(libs.mediarouter)
|
||||||
|
implementation(libs.playServicesCastFramework)
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
|
||||||
implementation(rootProject.files("libs/lib-decoder-ffmpeg-release.aar"))
|
implementation(rootProject.files("libs/lib-decoder-ffmpeg-release.aar"))
|
||||||
|
|
|
@ -33,6 +33,23 @@
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- android:theme="@style/Theme.CastVideosDark"-->
|
||||||
|
<activity
|
||||||
|
android:name=".chromecast.ExpandedControlsActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:parentActivityName=".PlayerActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name=
|
||||||
|
"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||||
|
android:value="dev.jdtech.jellyfin.chromecast.CastOptionsProvider" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -11,20 +11,31 @@ import androidx.navigation.ui.AppBarConfiguration
|
||||||
import androidx.navigation.ui.NavigationUI
|
import androidx.navigation.ui.NavigationUI
|
||||||
import androidx.navigation.ui.NavigationUiSaveStateControl
|
import androidx.navigation.ui.NavigationUiSaveStateControl
|
||||||
import androidx.navigation.ui.setupActionBarWithNavController
|
import androidx.navigation.ui.setupActionBarWithNavController
|
||||||
|
import com.google.android.gms.cast.framework.CastContext
|
||||||
|
import com.google.android.gms.cast.framework.CastSession
|
||||||
|
import com.google.android.gms.cast.framework.SessionManager
|
||||||
|
import com.google.android.gms.cast.framework.SessionManagerListener
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||||
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||||
import dev.jdtech.jellyfin.databinding.ActivityMainBinding
|
import dev.jdtech.jellyfin.databinding.ActivityMainBinding
|
||||||
import dev.jdtech.jellyfin.utils.loadDownloadLocation
|
import dev.jdtech.jellyfin.utils.loadDownloadLocation
|
||||||
import dev.jdtech.jellyfin.viewmodels.MainViewModel
|
import dev.jdtech.jellyfin.viewmodels.MainViewModel
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
|
||||||
private val viewModel: MainViewModel by viewModels()
|
private val viewModel: MainViewModel by viewModels()
|
||||||
|
private var castSession: CastSession? = null
|
||||||
|
private lateinit var sessionManager: SessionManager
|
||||||
|
private val sessionManagerListener: SessionManagerListener<CastSession> =
|
||||||
|
SessionManagerListenerImpl(this)
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var database: ServerDatabaseDao
|
lateinit var database: ServerDatabaseDao
|
||||||
|
@ -34,6 +45,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var navController: NavController
|
private lateinit var navController: NavController
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var jellyfinApi: JellyfinApi
|
||||||
|
|
||||||
@OptIn(NavigationUiSaveStateControl::class)
|
@OptIn(NavigationUiSaveStateControl::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -42,6 +56,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
setTheme(R.style.Theme_FindroidAMOLED)
|
setTheme(R.style.Theme_FindroidAMOLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionManager = CastContext.getSharedInstance(this).sessionManager
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
@ -91,6 +106,19 @@ class MainActivity : AppCompatActivity() {
|
||||||
loadDownloadLocation(applicationContext)
|
loadDownloadLocation(applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
castSession = sessionManager.currentCastSession
|
||||||
|
sessionManager.addSessionManagerListener(sessionManagerListener, CastSession::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
sessionManager.removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
|
||||||
|
castSession = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
return navController.navigateUp()
|
return navController.navigateUp()
|
||||||
}
|
}
|
||||||
|
@ -118,4 +146,45 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private class SessionManagerListenerImpl(private val mainActivity: MainActivity) :
|
||||||
|
SessionManagerListener<CastSession> {
|
||||||
|
override fun onSessionStarted(session: CastSession, sessionId: String) {
|
||||||
|
mainActivity.invalidateOptionsMenu()
|
||||||
|
val thing =
|
||||||
|
"{\"options\":{},\"command\":\"Identify\",\"userId\":\"${mainActivity.jellyfinApi.userId}\",\"deviceId\":\"${mainActivity.jellyfinApi.api.deviceInfo.id}\",\"accessToken\":\"${mainActivity.jellyfinApi.api.accessToken}\",\"serverAddress\":\"${mainActivity.jellyfinApi.api.baseUrl}\",\"serverId\":\"\",\"serverVersion\":\"\",\"receiverName\":\"\"}"
|
||||||
|
session.sendMessage("urn:x-cast:com.connectsdk", thing)
|
||||||
|
session.setMessageReceivedCallbacks(
|
||||||
|
"urn:x-cast:com.connectsdk"
|
||||||
|
) { _, _, message -> Timber.i(message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
|
||||||
|
mainActivity.invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionEnded(session: CastSession, error: Int) {
|
||||||
|
// finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionEnding(p0: CastSession) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionResumeFailed(p0: CastSession, p1: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionResuming(p0: CastSession, p1: String) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionStartFailed(p0: CastSession, p1: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionStarting(p0: CastSession) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSessionSuspended(p0: CastSession, p1: Int) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package dev.jdtech.jellyfin.chromecast
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.NotificationOptions
|
||||||
|
|
||||||
|
|
||||||
|
class CastOptionsProvider : OptionsProvider {
|
||||||
|
companion object {
|
||||||
|
const val CUSTOM_NAMESPACE = "urn:x-cast:com.connectsdk"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCastOptions(context: Context): CastOptions {
|
||||||
|
val supportedNamespaces: MutableList<String> = ArrayList()
|
||||||
|
supportedNamespaces.add(CUSTOM_NAMESPACE)
|
||||||
|
|
||||||
|
val notificationOptions = NotificationOptions.Builder()
|
||||||
|
.setTargetActivityClassName(ExpandedControlsActivity::class.java.name)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val mediaOptions = CastMediaOptions.Builder()
|
||||||
|
.setNotificationOptions(notificationOptions)
|
||||||
|
.setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return CastOptions.Builder()
|
||||||
|
// .setReceiverApplicationId("F007D354")
|
||||||
|
.setReceiverApplicationId("D991CC1E")
|
||||||
|
// .setSupportedNamespaces(supportedNamespaces)
|
||||||
|
.setCastMediaOptions(mediaOptions)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAdditionalSessionProviders(p0: Context): MutableList<SessionProvider>? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package dev.jdtech.jellyfin.chromecast
|
||||||
|
|
||||||
|
import android.view.Menu
|
||||||
|
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||||
|
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
|
||||||
|
import dev.jdtech.jellyfin.R
|
||||||
|
|
||||||
|
class ExpandedControlsActivity : ExpandedControllerActivity() {
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
menuInflater.inflate(R.menu.expanded_controller, menu)
|
||||||
|
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
|
import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
|
||||||
|
@ -65,6 +66,11 @@ class HomeFragment : Fragment() {
|
||||||
object : MenuProvider {
|
object : MenuProvider {
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
menuInflater.inflate(R.menu.home_menu, menu)
|
menuInflater.inflate(R.menu.home_menu, menu)
|
||||||
|
CastButtonFactory.setUpMediaRouteButton(
|
||||||
|
requireContext(),
|
||||||
|
menu,
|
||||||
|
R.id.media_route_menu_item
|
||||||
|
)
|
||||||
|
|
||||||
val settings = menu.findItem(R.id.action_settings)
|
val settings = menu.findItem(R.id.action_settings)
|
||||||
val search = menu.findItem(R.id.action_search)
|
val search = menu.findItem(R.id.action_search)
|
||||||
|
|
|
@ -18,6 +18,8 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
|
import androidx.recyclerview.widget.LinearSnapHelper
|
||||||
|
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.AppPreferences
|
import dev.jdtech.jellyfin.AppPreferences
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
|
@ -64,6 +66,11 @@ class LibraryFragment : Fragment() {
|
||||||
object : MenuProvider {
|
object : MenuProvider {
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
menuInflater.inflate(R.menu.library_menu, menu)
|
menuInflater.inflate(R.menu.library_menu, menu)
|
||||||
|
CastButtonFactory.setUpMediaRouteButton(
|
||||||
|
context!!,
|
||||||
|
menu,
|
||||||
|
R.id.media_route_menu_item
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.adapters.CollectionListAdapter
|
import dev.jdtech.jellyfin.adapters.CollectionListAdapter
|
||||||
|
@ -85,6 +86,11 @@ class MediaFragment : Fragment() {
|
||||||
object : MenuProvider {
|
object : MenuProvider {
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
menuInflater.inflate(R.menu.media_menu, menu)
|
menuInflater.inflate(R.menu.media_menu, menu)
|
||||||
|
CastButtonFactory.setUpMediaRouteButton(
|
||||||
|
requireContext(),
|
||||||
|
menu,
|
||||||
|
R.id.media_route_menu_item
|
||||||
|
)
|
||||||
|
|
||||||
val search = menu.findItem(R.id.action_search)
|
val search = menu.findItem(R.id.action_search)
|
||||||
val searchView = search.actionView as SearchView
|
val searchView = search.actionView as SearchView
|
||||||
|
|
|
@ -4,6 +4,19 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/cast_mini_controller"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:castShowImageThumbnail="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/nav_view"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" />
|
||||||
|
|
||||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
android:id="@+id/nav_view"
|
android:id="@+id/nav_view"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -21,7 +34,7 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:defaultNavHost="true"
|
app:defaultNavHost="true"
|
||||||
app:layout_constraintBottom_toTopOf="@id/nav_view"
|
app:layout_constraintBottom_toTopOf="@id/cast_mini_controller"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
|
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
|
||||||
|
|
10
core/src/main/res/menu/expanded_controller.xml
Normal file
10
core/src/main/res/menu/expanded_controller.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/media_route_menu_item"
|
||||||
|
android:title="@string/media_route_menu_title"
|
||||||
|
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
|
||||||
|
app:showAsAction="always"/>
|
||||||
|
|
||||||
|
</menu>
|
|
@ -10,7 +10,11 @@
|
||||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||||
app:showAsAction="always|collapseActionView"
|
app:showAsAction="always|collapseActionView"
|
||||||
tools:ignore="AlwaysShowAction" />
|
tools:ignore="AlwaysShowAction" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/media_route_menu_item"
|
||||||
|
android:title="@string/media_route_menu_title"
|
||||||
|
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
|
||||||
|
app:showAsAction="always" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_settings"
|
android:id="@+id/action_settings"
|
||||||
android:icon="@drawable/ic_settings"
|
android:icon="@drawable/ic_settings"
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/media_route_menu_item"
|
||||||
|
android:title="@string/media_route_menu_title"
|
||||||
|
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
|
||||||
<!--Icon is currently not used but maybe in the future?-->
|
<!--Icon is currently not used but maybe in the future?-->
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_sort_by"
|
android:id="@+id/action_sort_by"
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/media_route_menu_item"
|
||||||
|
android:title="@string/media_route_menu_title"
|
||||||
|
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_search"
|
android:id="@+id/action_search"
|
||||||
android:icon="@drawable/ic_search"
|
android:icon="@drawable/ic_search"
|
||||||
|
|
|
@ -163,4 +163,5 @@
|
||||||
<string name="dolby_logo_desc">Dolby Logo</string>
|
<string name="dolby_logo_desc">Dolby Logo</string>
|
||||||
<string name="extra_info">Display Extra Info</string>
|
<string name="extra_info">Display Extra Info</string>
|
||||||
<string name="extra_info_summary">Displays detailed information about Audio, Video and Subtitles</string>
|
<string name="extra_info_summary">Displays detailed information about Audio, Video and Subtitles</string>
|
||||||
|
<string name="media_route_menu_title">Cast Thing</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -46,6 +46,18 @@
|
||||||
<item name="dialogCornerRadius">28dp</item>
|
<item name="dialogCornerRadius">28dp</item>
|
||||||
<item name="preferenceTheme">@style/ThemeOverlay.Findroid.Preference</item>
|
<item name="preferenceTheme">@style/ThemeOverlay.Findroid.Preference</item>
|
||||||
<item name="dolbyColor">#000</item>
|
<item name="dolbyColor">#000</item>
|
||||||
|
|
||||||
|
<!-- Cast SDK -->
|
||||||
|
<item name="castMiniControllerStyle">@style/CustomCastMiniController</item>
|
||||||
|
<item name="castExpandedControllerStyle">@style/CustomCastExpandedController</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="CustomCastMiniController" parent="CastMiniController">
|
||||||
|
<item name="castProgressBarColor">?attr/colorPrimary</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="CustomCastExpandedController" parent="CastExpandedController">
|
||||||
|
<item name="castSeekBarProgressAndThumbColor">?attr/colorPrimary</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<string-array name="themes">
|
<string-array name="themes">
|
||||||
|
|
|
@ -24,6 +24,8 @@ ktlint = "11.1.0"
|
||||||
libmpv = "0.1.1"
|
libmpv = "0.1.1"
|
||||||
material = "1.8.0"
|
material = "1.8.0"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
|
androidx-mediarouter = "1.3.1"
|
||||||
|
play-services-cast-framework = "21.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" }
|
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" }
|
||||||
|
@ -57,6 +59,8 @@ hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hil
|
||||||
jellyfin-core = { module = "org.jellyfin.sdk:jellyfin-core", version.ref = "jellyfin" }
|
jellyfin-core = { module = "org.jellyfin.sdk:jellyfin-core", version.ref = "jellyfin" }
|
||||||
libmpv = { module = "dev.jdtech.mpv:libmpv", version.ref = "libmpv" }
|
libmpv = { module = "dev.jdtech.mpv:libmpv", version.ref = "libmpv" }
|
||||||
material = { module = "com.google.android.material:material", version.ref = "material" }
|
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||||
|
mediarouter = { module = "androidx.mediarouter:mediarouter", version.ref = "androidx-mediarouter" }
|
||||||
|
playServicesCastFramework = { module = "com.google.android.gms:play-services-cast-framework", version.ref = "play-services-cast-framework"}
|
||||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||||
|
|
||||||
|
|
|
@ -57,4 +57,5 @@ dependencies {
|
||||||
implementation(libs.libmpv)
|
implementation(libs.libmpv)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
|
implementation(libs.playServicesCastFramework)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
import com.google.android.gms.cast.framework.CastContext
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||||
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
|
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
|
||||||
import dev.jdtech.jellyfin.models.ExternalSubtitle
|
import dev.jdtech.jellyfin.models.ExternalSubtitle
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
|
@ -18,6 +22,7 @@ import javax.inject.Inject
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
@ -30,7 +35,9 @@ import timber.log.Timber
|
||||||
class PlayerViewModel @Inject internal constructor(
|
class PlayerViewModel @Inject internal constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val repository: JellyfinRepository,
|
private val repository: JellyfinRepository,
|
||||||
private val downloadDatabase: DownloadDatabaseDao
|
private val downloadDatabase: DownloadDatabaseDao,
|
||||||
|
private val jellyfinApi: JellyfinApi,
|
||||||
|
@ApplicationContext private val context: Context
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val playerItems = MutableSharedFlow<PlayerItemState>(
|
private val playerItems = MutableSharedFlow<PlayerItemState>(
|
||||||
|
@ -61,6 +68,17 @@ class PlayerViewModel @Inject internal constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
val session = CastContext.getSharedInstance(context).sessionManager.currentCastSession
|
||||||
|
|
||||||
|
if (session != null) {
|
||||||
|
val thing =
|
||||||
|
"{\"options\":{\"ids\":[\"${item.id}\"],\"startPositionTicks\":${
|
||||||
|
item.userData?.playbackPositionTicks ?: 0
|
||||||
|
},\"serverId\":\"\",\"fullscreen\":true,\"items\":[{\"Id\":\"${item.id}\",\"ServerId\":\"\",\"Name\":\"${item.name}\",\"Type\":\"${item.type}\",\"MediaType\":\"${item.mediaType}\",\"IsFolder\":false}]},\"command\":\"PlayNow\",\"userId\":\"${jellyfinApi.userId}\",\"deviceId\":\"${jellyfinApi.api.deviceInfo.id}\",\"accessToken\":\"${jellyfinApi.api.accessToken}\",\"serverAddress\":\"${jellyfinApi.api.baseUrl}\",\"serverId\":\"\",\"serverVersion\":\"\",\"receiverName\":\"Living Room TV\",\"subtitleAppearance\":{\"verticalPosition\":-3},\"subtitleBurnIn\":\"\"}"
|
||||||
|
session.sendMessage("urn:x-cast:com.connectsdk", thing)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val playbackPosition = item.userData?.playbackPositionTicks?.div(10000) ?: 0
|
val playbackPosition = item.userData?.playbackPositionTicks?.div(10000) ?: 0
|
||||||
|
|
||||||
val items = try {
|
val items = try {
|
||||||
|
@ -69,7 +87,6 @@ class PlayerViewModel @Inject internal constructor(
|
||||||
Timber.d(e)
|
Timber.d(e)
|
||||||
PlayerItemError(e)
|
PlayerItemError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
playerItems.tryEmit(items)
|
playerItems.tryEmit(items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue