Major refactoring
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Balazs Toldi 2021-11-23 16:28:33 +01:00
parent 01b883ed27
commit cf4374d34b
Signed by: Bazsalanszky
GPG key ID: 933820884952BE27
14 changed files with 648 additions and 127 deletions

View file

@ -26,6 +26,8 @@
<entry key="../../../../../layout/compose-model-1637654654111.xml" value="0.33" /> <entry key="../../../../../layout/compose-model-1637654654111.xml" value="0.33" />
<entry key="../../../../../layout/compose-model-1637655458616.xml" value="0.45740740740740743" /> <entry key="../../../../../layout/compose-model-1637655458616.xml" value="0.45740740740740743" />
<entry key="../../../../../layout/compose-model-1637656156044.xml" value="0.25" /> <entry key="../../../../../layout/compose-model-1637656156044.xml" value="0.25" />
<entry key="../../../../../layout/compose-model-1637673325505.xml" value="0.11570945945945946" />
<entry key="../../../../../layout/compose-model-1637674242617.xml" value="0.49537037037037035" />
</map> </map>
</option> </option>
</component> </component>

View file

@ -49,21 +49,22 @@ android {
dependencies { dependencies {
implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0' implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation "androidx.compose.material:material-icons-extended:$compose_version" implementation "androidx.compose.material:material-icons-extended:$compose_version"
implementation "androidx.compose.runtime:runtime-livedata:$compose_version" implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
implementation 'androidx.activity:activity-compose:1.3.1' implementation 'androidx.activity:activity-compose:1.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
implementation 'su.litvak.chromecast:api-v2:0.11.3' implementation 'su.litvak.chromecast:api-v2:0.11.3'
implementation 'com.github.yausername.youtubedl-android:library:0.12.+' implementation 'com.github.yausername.youtubedl-android:library:0.12.+'
implementation 'com.github.yausername.youtubedl-android:ffmpeg:0.12.+' implementation 'com.github.yausername.youtubedl-android:ffmpeg:0.12.+'
implementation "androidx.media:media:1.4.3"
testImplementation 'junit:junit:4.+' testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View file

@ -4,8 +4,10 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application <application
android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_caster_logo" android:icon="@drawable/ic_caster_logo"
android:label="@string/app_name" android:label="@string/app_name"
@ -27,7 +29,12 @@
android:name=".ChromecastManagerActivity" android:name=".ChromecastManagerActivity"
android:exported="true" android:exported="true"
android:label="@string/title_activity_chromecast_manager" android:label="@string/title_activity_chromecast_manager"
android:theme="@style/Theme.Caster.NoActionBar" /> android:theme="@style/Theme.Caster.NoActionBar" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</activity>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@ -39,6 +46,7 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name=".services.ChromecastManagerService"/>
</application> </application>
</manifest> </manifest>

View file

@ -0,0 +1,21 @@
package eu.toldi.balazs.caster
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
class App : Application() {
companion object {
const val CHANNEL_ID = "casterServiceNotificationChannel"
}
override fun onCreate() {
super.onCreate()
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(CHANNEL_ID,"Caster Service Channel",NotificationManager.IMPORTANCE_LOW)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
}
}

View file

@ -1,5 +1,17 @@
package eu.toldi.balazs.caster package eu.toldi.balazs.caster
import android.util.Log
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLException
import com.yausername.youtubedl_android.YoutubeDLRequest
import com.yausername.youtubedl_android.mapper.VideoInfo
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import su.litvak.chromecast.api.v2.Application
import su.litvak.chromecast.api.v2.ChromeCast
import su.litvak.chromecast.api.v2.MediaStatus
object ChromeCastHelper { object ChromeCastHelper {
const val APP_BACKDROP = "E8C28D3C" const val APP_BACKDROP = "E8C28D3C"
@ -13,4 +25,125 @@ object ChromeCastHelper {
const val APP_BUBBLEUPNP = "3927FA74" const val APP_BUBBLEUPNP = "3927FA74"
const val APP_BBCSOUNDS = "03977A48" const val APP_BBCSOUNDS = "03977A48"
const val APP_BBCIPLAYER = "5E81F6DB" const val APP_BBCIPLAYER = "5E81F6DB"
lateinit var chromeCast: ChromeCast
fun getApplicationDisplayName(id: String): String {
return when (id) {
APP_MEDIA_RECEIVER -> "Media Player"
APP_YOUTUBE -> "Youtube"
APP_PLEX -> "Plex"
APP_BACKDROP -> "Idle"
APP_DASHCAST -> "Dashcast"
APP_HOME_ASSISTANT -> "Home Assistant"
APP_SUPLA -> "Supla"
APP_YLEAREENA -> "Yleareena"
APP_BUBBLEUPNP -> "BubbleUPNP"
APP_BBCSOUNDS -> "BBC Sounds"
APP_BBCIPLAYER -> "BBC IPlayer"
else -> "Unknown App $id"
}
}
suspend fun fetchMediaStatus(): MediaStatus? {
try {
var mediaStatus : MediaStatus? = null
withContext(IO) {
val status = chromeCast.status
if (status.runningApp != null) {
mediaStatus = chromeCast.mediaStatus
}
}
return mediaStatus
} catch (e: Exception) {
Log.e(null, e.stackTraceToString())
}
return null
}
suspend fun seek(d: Double) {
val mediaStatus = fetchMediaStatus()
if (mediaStatus != null) {
try {
withContext(IO) {
chromeCast.seek(d * mediaStatus.media.duration)
}
} catch (e: Exception) {
Log.e(null, e.stackTraceToString())
}
}
}
suspend fun stopApp() {
withContext(IO) {
if (chromeCast.runningApp != null) {
chromeCast.stopApp()
}
}
}
suspend fun play() {
withContext(IO) {
chromeCast.play()
}
}
suspend fun pause(){
withContext(IO) {
chromeCast.pause()
}
}
suspend fun setVolume(f: Float) {
withContext(IO) {
chromeCast.setVolume(f)
}
}
suspend fun castLink(link : String, callBack : () -> Unit = {}) : Boolean{
var exitStatus = true
if(link.startsWith("https://").not() && link.startsWith("http://").not()) {
callBack.invoke()
return false
}
withContext(IO){
val request =
YoutubeDLRequest(link)
request.addOption("-f", "best")
val _streamInfo = try {
YoutubeDL.getInstance().getInfo(request)
} catch (e: YoutubeDLException) {
null
}
if (_streamInfo == null) {
exitStatus = false
callBack.invoke()
} else {
val streamInfo = _streamInfo as VideoInfo
val status = chromeCast.status
if (chromeCast.isAppAvailable(ChromeCastHelper.APP_MEDIA_RECEIVER) && !status.isAppRunning(
ChromeCastHelper.APP_MEDIA_RECEIVER
)
) {
val app: Application = chromeCast.launchApp(ChromeCastHelper.APP_MEDIA_RECEIVER)
}
while (!chromeCast.status.isAppRunning(ChromeCastHelper.APP_MEDIA_RECEIVER)) {
delay(100)
}
chromeCast.load(
streamInfo.title,
streamInfo.thumbnail,
streamInfo.url,
null
)
callBack.invoke()
}
}
return exitStatus
}
} }

View file

@ -1,10 +1,13 @@
package eu.toldi.balazs.caster package eu.toldi.balazs.caster
import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -14,6 +17,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import eu.toldi.balazs.caster.ui.theme.CasterTheme import eu.toldi.balazs.caster.ui.theme.CasterTheme
@ -29,6 +34,7 @@ import com.yausername.youtubedl_android.YoutubeDLRequest
import com.yausername.youtubedl_android.YoutubeDLException import com.yausername.youtubedl_android.YoutubeDLException
import com.yausername.youtubedl_android.mapper.VideoInfo import com.yausername.youtubedl_android.mapper.VideoInfo
import eu.toldi.balazs.caster.model.ChromecastManageViewmodel import eu.toldi.balazs.caster.model.ChromecastManageViewmodel
import eu.toldi.balazs.caster.services.ChromecastManagerService
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import su.litvak.chromecast.api.v2.Media import su.litvak.chromecast.api.v2.Media
import su.litvak.chromecast.api.v2.MediaStatus import su.litvak.chromecast.api.v2.MediaStatus
@ -37,69 +43,31 @@ import su.litvak.chromecast.api.v2.MediaStatus
class ChromecastManagerActivity : ComponentActivity() { class ChromecastManagerActivity : ComponentActivity() {
companion object { companion object {
var chromeCast_: ChromeCast? = null var chromeCast_: ChromeCast? = null
suspend fun castLink(chromeCast: ChromeCast,link : String, callBack : () -> Unit = {}) : Boolean{
var exitStatus = true
if(link.startsWith("https://").not() && link.startsWith("http://").not()) {
callBack.invoke()
return false
}
withContext(IO){
val request =
YoutubeDLRequest(link)
request.addOption("-f", "best")
val _streamInfo = try {
YoutubeDL.getInstance().getInfo(request)
} catch (e: YoutubeDLException) {
null
}
if (_streamInfo == null) {
exitStatus = false
callBack.invoke()
} else {
val streamInfo = _streamInfo as VideoInfo
val status = chromeCast.status
if (chromeCast.isAppAvailable(ChromeCastHelper.APP_MEDIA_RECEIVER) && !status.isAppRunning(
ChromeCastHelper.APP_MEDIA_RECEIVER
)
) {
val app: Application = chromeCast.launchApp(ChromeCastHelper.APP_MEDIA_RECEIVER)
}
while (!chromeCast.status.isAppRunning(ChromeCastHelper.APP_MEDIA_RECEIVER)) {
delay(100)
}
chromeCast.load(
streamInfo.title,
streamInfo.thumbnail,
streamInfo.url,
null
)
callBack.invoke()
}
}
return exitStatus
}
} }
private lateinit var chromeCast: ChromeCast private lateinit var chromeCast: ChromeCast
private lateinit var viewModel : ChromecastManageViewmodel
@RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (chromeCast_ == null) if (chromeCast_ == null)
finish() finish()
chromeCast = chromeCast_ as ChromeCast chromeCast = chromeCast_ as ChromeCast
ChromeCastHelper.chromeCast = chromeCast
try { try {
YoutubeDL.getInstance().init(application) YoutubeDL.getInstance().init(application)
} catch (e: YoutubeDLException) { } catch (e: YoutubeDLException) {
Log.e("Caster", "failed to initialize youtubedl-android", e) Log.e("Caster", "failed to initialize youtubedl-android", e)
} }
val serviceIntent = Intent(this, ChromecastManagerService::class.java).also {
it.action = ChromecastManagerService.ACTION_INIT
it.putExtra("CHROMECAST_ADDRESS", chromeCast.address)
it.putExtra("CHROMECAST_NAME", chromeCast.title)
ContextCompat.startForegroundService(this, it)
}
setContent { setContent {
CasterTheme { CasterTheme {
val viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java) viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java)
viewModel.chromeCast = chromeCast viewModel.chromeCast = chromeCast
// A surface container using the 'background' color from the theme // A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) { Surface(color = MaterialTheme.colors.background) {
@ -123,38 +91,53 @@ class ChromecastManagerActivity : ComponentActivity() {
label = { Text("Cast URL") }, label = { Text("Cast URL") },
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
) )
var castEnabled by remember { mutableStateOf(true)} var castEnabled by remember { mutableStateOf(true) }
Button( Button(
enabled = castEnabled, enabled = castEnabled,
onClick = { onClick = {
castEnabled = false castEnabled = false
lifecycleScope.launch { viewModel.castLink(text) {
castLink(chromeCast, text) { castEnabled = true
castEnabled = true
}
} }
}, },
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
) { ) {
Text(text = "Cast") Text(text = "Cast")
} }
if(castEnabled.not()){ if (castEnabled.not()) {
CircularProgressIndicator() CircularProgressIndicator()
} }
playBackControl(viewmodel = viewModel) playBackControl()
val mediaStatus = viewModel.mediaStatus.value val mediaStatus = viewModel.mediaStatus.value
if(mediaStatus != null) { if (mediaStatus != null) {
val nowPlaying = mediaStatus.media.metadata[Media.METADATA_TITLE] val nowPlaying = mediaStatus.media.metadata[Media.METADATA_TITLE]
Text(text = "Now playing: $nowPlaying") Text(text = "Now playing: $nowPlaying")
val progress = mediaStatus.currentTime/ mediaStatus.media.duration var sliderPosition by remember { mutableStateOf((mediaStatus.currentTime / mediaStatus.media.duration).toFloat()) }
Row(Modifier.fillMaxWidth(),horizontalArrangement = Arrangement.SpaceBetween) {
Text(text = String.format("%02d:%02d",((mediaStatus.currentTime % 3600) / 60).toInt(), (mediaStatus.currentTime % 60).toInt())) Row(
Text(text = String.format("%02d:%02d",((mediaStatus.media.duration % 3600) / 60).toInt(),(mediaStatus.media.duration % 60).toInt())) Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = String.format(
"%02d:%02d",
((mediaStatus.currentTime % 3600) / 60).toInt(),
(mediaStatus.currentTime % 60).toInt()
)
)
Text(
text = String.format(
"%02d:%02d",
((mediaStatus.media.duration % 3600) / 60).toInt(),
(mediaStatus.media.duration % 60).toInt()
)
)
} }
Slider(value = progress.toFloat(), onValueChange = { Slider(value = sliderPosition, onValueChange = {
viewModel.seek(it.toDouble()) sliderPosition = it
}, onValueChangeFinished = {
viewModel.seek(sliderPosition.toDouble())
}) })
} }
} }
@ -188,18 +171,15 @@ class ChromecastManagerActivity : ComponentActivity() {
} }
@Composable @Composable
fun playBackControl(viewmodel: ChromecastManageViewmodel){ fun playBackControl() {
val mediaStatus = viewmodel.mediaStatus.value val mediaStatus = viewModel.mediaStatus.value
Row { Row {
IconButton(onClick = { IconButton(onClick = {
if(mediaStatus != null) { if (mediaStatus != null) {
lifecycleScope.launch(IO) { viewModel.seek(chromeCast.mediaStatus.currentTime - 10)
chromeCast.seek(chromeCast.mediaStatus.currentTime - 10) }
viewmodel.fetchMediaStatus() }, enabled = mediaStatus != null) {
}
}
},enabled = mediaStatus != null) {
Icon( Icon(
Icons.Filled.FastRewind, Icons.Filled.FastRewind,
contentDescription = "FastRewind" contentDescription = "FastRewind"
@ -212,18 +192,18 @@ class ChromecastManagerActivity : ComponentActivity() {
playBackState = chromeCast.mediaStatus.playerState playBackState = chromeCast.mediaStatus.playerState
}*/ }*/
IconButton(onClick = { IconButton(onClick = {
lifecycleScope.launch(IO) {
if(mediaStatus != null) { if (mediaStatus != null) {
if (mediaStatus.playerState == MediaStatus.PlayerState.PLAYING) { if (mediaStatus.playerState == MediaStatus.PlayerState.PLAYING) {
chromeCast.pause() viewModel.pause()
}
if (mediaStatus.playerState == MediaStatus.PlayerState.PAUSED) {
chromeCast.play()
}
viewmodel.fetchMediaStatus()
} }
if (mediaStatus.playerState == MediaStatus.PlayerState.PAUSED) {
viewModel.play()
}
viewModel.fetchMediaStatus()
} }
},enabled = mediaStatus != null) {
}, enabled = mediaStatus != null) {
when { when {
mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.PAUSED -> Icon( mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.PAUSED -> Icon(
Icons.Filled.PlayArrow, Icons.Filled.PlayArrow,
@ -242,13 +222,8 @@ class ChromecastManagerActivity : ComponentActivity() {
} }
IconButton(onClick = { IconButton(onClick = {
lifecycleScope.launch(IO) { viewModel.stopApp()
if(chromeCast.runningApp != null) { }, enabled = mediaStatus != null) {
chromeCast.stopApp()
viewmodel.fetchMediaStatus()
}
}
},enabled = mediaStatus != null) {
Icon( Icon(
Icons.Filled.Stop, Icons.Filled.Stop,
contentDescription = "Stop" contentDescription = "Stop"
@ -256,13 +231,10 @@ class ChromecastManagerActivity : ComponentActivity() {
} }
IconButton(onClick = { IconButton(onClick = {
lifecycleScope.launch(IO) { if (chromeCast.mediaStatus != null) {
if(chromeCast.mediaStatus != null) { viewModel.seek(chromeCast.mediaStatus.currentTime + 10)
chromeCast.seek(chromeCast.mediaStatus.currentTime + 10)
viewmodel.fetchMediaStatus()
}
} }
},enabled = mediaStatus != null) { }, enabled = mediaStatus != null) {
Icon( Icon(
Icons.Filled.FastForward, Icons.Filled.FastForward,
contentDescription = "FastForward" contentDescription = "FastForward"
@ -302,26 +274,12 @@ class ChromecastManagerActivity : ComponentActivity() {
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
) )
Button( Button(
onClick = { onClick = {},
GlobalScope.launch(IO) {
val status = chromeCast.getStatus()
if (chromeCast.isAppAvailable("CC1AD845") && !status.isAppRunning(
"CC1AD845"
)
) {
val app: Application = chromeCast.launchApp("CC1AD845")
}
while (!status.isAppRunning("CC1AD845")) {
delay(100)
}
chromeCast.load(text)
}
},
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
) { ) {
Text(text = "Cast") Text(text = "Cast")
} }
// val viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java) // val viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java)
//playBackControl(viewModel) //playBackControl(viewModel)
Text(text = "Now playing: Some video") Text(text = "Now playing: Some video")
var sliderPosition by remember { mutableStateOf(0f) } var sliderPosition by remember { mutableStateOf(0f) }
@ -332,16 +290,14 @@ class ChromecastManagerActivity : ComponentActivity() {
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if(keyCode == KeyEvent.KEYCODE_VOLUME_UP) { if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
GlobalScope.launch(IO) { viewModel.setVolume(chromeCast.status.volume.level + 0.05f)
chromeCast.setVolume(chromeCast.status.volume.level + 0.05f)
}
return true return true
} }
if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
GlobalScope.launch(IO) {
chromeCast.setVolume(chromeCast.status.volume.level - 0.05f) viewModel.setVolume(chromeCast.status.volume.level - 0.05f)
}
return true return true
} }
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)

View file

@ -107,7 +107,8 @@ class ShareRecieverActivity : ComponentActivity() {
onClick = { onClick = {
onEnableChanged(false) onEnableChanged(false)
lifecycleScope.launch { lifecycleScope.launch {
ChromecastManagerActivity.castLink(chromeCast, link) { ChromeCastHelper.chromeCast = chromeCast
ChromeCastHelper.castLink(link) {
finish() finish()
} }
} }

View file

@ -0,0 +1,83 @@
package eu.toldi.balazs.caster.model
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import eu.toldi.balazs.caster.ChromeCastHelper
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import su.litvak.chromecast.api.v2.ChromeCast
import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEvent
import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEventListener
import su.litvak.chromecast.api.v2.MediaStatus
class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListener {
private val _mediaState = mutableStateOf<MediaStatus?>(null)
val mediaStatus: State<MediaStatus?>
get() = _mediaState
lateinit var chromeCast: ChromeCast
init {
viewModelScope.launch(IO) {
while (true) {
fetchMediaStatus()
Thread.sleep(1000)
}
}
}
override fun spontaneousEventReceived(event: ChromeCastSpontaneousEvent?) {
Log.e(null, event?.type.toString())
if (event != null) {
if (event.type == ChromeCastSpontaneousEvent.SpontaneousEventType.MEDIA_STATUS) {
_mediaState.value = event.data as MediaStatus
}
}
}
fun fetchMediaStatus() {
viewModelScope.launch(IO) {
_mediaState.value = ChromeCastHelper.fetchMediaStatus()
}
}
fun seek(d: Double) {
viewModelScope.launch(IO) {
ChromeCastHelper.seek(d)
}
}
fun castLink(link : String,callBack: () -> Unit = {}) {
viewModelScope.launch(IO) {
ChromeCastHelper.castLink(link,callBack)
}
}
fun stopApp() {
viewModelScope.launch(IO) {
ChromeCastHelper.stopApp()
}
}
fun play() {
viewModelScope.launch(IO) {
ChromeCastHelper.play()
}
}
fun pause() {
viewModelScope.launch(IO) {
ChromeCastHelper.pause()
}
}
fun setVolume(f: Float) {
viewModelScope.launch(IO) {
ChromeCastHelper.setVolume(f)
}
}
}

View file

@ -0,0 +1,276 @@
package eu.toldi.balazs.caster.services
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.media.MediaPlayer
import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.os.Build
import android.os.IBinder
import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import eu.toldi.balazs.caster.App.Companion.CHANNEL_ID
import eu.toldi.balazs.caster.ChromeCastHelper
import eu.toldi.balazs.caster.R
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import su.litvak.chromecast.api.v2.ChromeCast
import su.litvak.chromecast.api.v2.Media
import su.litvak.chromecast.api.v2.MediaStatus
class ChromecastManagerService : Service() {
companion object {
const val ACTION_INIT = "action_init"
const val ACTION_PLAY = "action_play"
const val ACTION_PAUSE = "action_pause"
const val ACTION_REWIND = "action_rewind"
const val ACTION_FAST_FORWARD = "action_fast_foward"
const val ACTION_NEXT = "action_next"
const val ACTION_PREVIOUS = "action_previous"
const val ACTION_STOP = "action_stop"
}
private val mMediaPlayer = MediaPlayer()
private lateinit var mSession: MediaSessionCompat
private lateinit var mController: MediaControllerCompat
private lateinit var pendingIntent : PendingIntent
private var mediaStatus: MediaStatus? = null
override fun onBind(p0: Intent?): IBinder? = null
private lateinit var chromeCast: ChromeCast
@RequiresApi(Build.VERSION_CODES.M)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when(intent!!.action) {
ACTION_INIT -> initAction(intent)
ACTION_PLAY -> mController.transportControls.play()
ACTION_PAUSE -> mController.transportControls.pause()
ACTION_FAST_FORWARD -> mController.transportControls.fastForward()
ACTION_REWIND -> mController.transportControls.rewind()
}
return START_REDELIVER_INTENT
}
private fun initAction(intent: Intent?){
initMediaSessions()
val address =
intent?.getStringExtra("CHROMECAST_ADDRESS") ?: throw IllegalArgumentException()
val name = intent?.getStringExtra("CHROMECAST_NAME") ?: "Chromecast"
Log.e(null, address)
chromeCast = ChromeCast(address)
chromeCast.name = name
val notificationIntent = Intent(this, ChromecastManagerService::class.java)
pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Caster for ${chromeCast.name}@${address}")
.setSmallIcon(R.drawable.ic_caster_logo)
.setContentIntent(pendingIntent).build()
startForeground(1, notification)
GlobalScope.launch(IO) {
while (true) {
try {
ChromeCastHelper.chromeCast = chromeCast
var status = chromeCast.status
Log.d(null, status.applications.toString())
val status_message = if (status.runningApp != null) {
ChromeCastHelper.getApplicationDisplayName(status.runningApp.id)
} else {
"${chromeCast.name} is ready for casting"
}
mediaStatus = ChromeCastHelper.fetchMediaStatus()
buildNotification()
} catch (e: Exception) {
Log.e(null,e.stackTraceToString())
}
Thread.sleep(1000)
}
}
}
private fun generateAction(
icon: Int,
title: String,
intentAction: String
): NotificationCompat.Action {
val intent = Intent(applicationContext, ChromecastManagerService::class.java)
intent.action = intentAction
val pendingIntent = PendingIntent.getService(applicationContext, 1, intent, 0)
return NotificationCompat.Action(icon,title,pendingIntent)
}
private suspend fun buildNotification() {
var status = chromeCast.status
Log.d(null, status.applications.toString())
val status_message = if (status.runningApp != null) {
ChromeCastHelper.getApplicationDisplayName(status.runningApp.id)
} else {
"${chromeCast.name} is ready for xcasting"
}
val notification: Notification
val mNotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (mediaStatus != null) {
notification = if(mediaStatus!!.playerState == MediaStatus.PlayerState.PLAYING) {
NotificationCompat.Builder(baseContext, CHANNEL_ID)
.setContentTitle(status_message)
.setSmallIcon(R.drawable.ic_caster_logo)
.setContentText(mediaStatus!!.media.metadata[Media.METADATA_TITLE].toString())
.addAction(
generateAction(
R.drawable.rewind,
"Rewind",
ACTION_REWIND
)
)
.addAction(
generateAction(
R.drawable.pause,
"Pause",
ACTION_PAUSE
)
)
.addAction(
generateAction(
R.drawable.forward,
"Fast forward",
ACTION_FAST_FORWARD
)
)
// Apply the media style template
.setStyle(androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(1 /* #1: pause button \*/)
.setMediaSession(mSession.sessionToken))
.setContentIntent(pendingIntent).build()
} else {
NotificationCompat.Builder(baseContext, CHANNEL_ID)
.setContentTitle(status_message)
.setSmallIcon(R.drawable.ic_caster_logo)
.setContentText(mediaStatus!!.media.metadata[Media.METADATA_TITLE].toString())
.addAction(
generateAction(
android.R.drawable.ic_media_rew,
"Rewind",
ACTION_REWIND
)
)
.addAction(
generateAction(
android.R.drawable.ic_media_play,
"Pause",
ACTION_PLAY
)
)
.addAction(
generateAction(
android.R.drawable.ic_media_ff,
"Fast forward",
ACTION_FAST_FORWARD
)
)
// Apply the media style template
.setStyle(androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(1 /* #1: pause button \*/)
.setMediaSession(mSession.sessionToken))
.setContentIntent(pendingIntent).build()
}
withContext(Dispatchers.Main) {
mNotificationManager.notify(1, notification)
}
} else {
notification = NotificationCompat.Builder(baseContext, CHANNEL_ID)
.setContentTitle(status_message)
.setSmallIcon(R.drawable.ic_caster_logo)
.setContentIntent(pendingIntent).build()
withContext(Dispatchers.Main) {
mNotificationManager.notify(1, notification)
}
}
}
private fun initMediaSessions() {
mSession = MediaSessionCompat(applicationContext, "simple player session")
mController = MediaControllerCompat(applicationContext, mSession.sessionToken)
mSession.setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() {
super.onPlay()
Log.e("MediaPlayerService", "onPlay")
GlobalScope.launch(IO) {
if(mediaStatus != null)
ChromeCastHelper.play()
}
}
override fun onPause() {
super.onPause()
Log.e("MediaPlayerService", "onPause")
GlobalScope.launch(IO) {
if(mediaStatus != null)
ChromeCastHelper.pause()
}
}
override fun onFastForward() {
super.onFastForward()
Log.e("MediaPlayerService", "onFastForward")
GlobalScope.launch(IO) {
val mediaStatus = ChromeCastHelper.fetchMediaStatus()
if(mediaStatus != null)
ChromeCastHelper.seek(mediaStatus.currentTime+10)
}
//Manipulate current media here
}
override fun onRewind() {
super.onRewind()
Log.e("MediaPlayerService", "onRewind")
GlobalScope.launch(IO) {
val mediaStatus = ChromeCastHelper.fetchMediaStatus()
if(mediaStatus != null)
ChromeCastHelper.seek(mediaStatus.currentTime-10)
}
}
override fun onStop() {
super.onStop()
Log.e("MediaPlayerService", "onStop")
//Stop media player here
stopForeground(true)
val intent = Intent(applicationContext, ChromecastManagerService::class.java)
stopService(intent)
}
}
)
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M8,5v14l11,-7z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/>
</vector>

View file

@ -24,7 +24,7 @@ ext.getVersionCode = { ->
try { try {
def stdout = new ByteArrayOutputStream() def stdout = new ByteArrayOutputStream()
exec { exec {
commandLine 'git', 'rev-list', '--first-parent', '--count', 'origin/main' commandLine 'git', 'rev-list', '--first-parent', '--count', '--all'
standardOutput = stdout standardOutput = stdout
} }
return Integer.parseInt(stdout.toString().trim()) return Integer.parseInt(stdout.toString().trim())