This commit is contained in:
parent
01b883ed27
commit
cf4374d34b
14 changed files with 648 additions and 127 deletions
|
@ -26,6 +26,8 @@
|
|||
<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-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>
|
||||
</option>
|
||||
</component>
|
||||
|
|
|
@ -49,21 +49,22 @@ android {
|
|||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation "androidx.compose.ui:ui:$compose_version"
|
||||
implementation "androidx.compose.material:material:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation "androidx.compose.material:material-icons-extended:$compose_version"
|
||||
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||
implementation 'androidx.activity:activity-compose:1.3.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
|
||||
implementation 'androidx.activity:activity-compose:1.4.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
|
||||
implementation 'su.litvak.chromecast:api-v2:0.11.3'
|
||||
implementation 'com.github.yausername.youtubedl-android:library:0.12.+'
|
||||
implementation 'com.github.yausername.youtubedl-android:ffmpeg:0.12.+'
|
||||
implementation "androidx.media:media:1.4.3"
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_caster_logo"
|
||||
android:label="@string/app_name"
|
||||
|
@ -27,7 +29,12 @@
|
|||
android:name=".ChromecastManagerActivity"
|
||||
android:exported="true"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
@ -39,6 +46,7 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service android:name=".services.ChromecastManagerService"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
21
app/src/main/java/eu/toldi/balazs/caster/App.kt
Normal file
21
app/src/main/java/eu/toldi/balazs/caster/App.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,17 @@
|
|||
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 {
|
||||
|
||||
const val APP_BACKDROP = "E8C28D3C"
|
||||
|
@ -13,4 +25,125 @@ object ChromeCastHelper {
|
|||
const val APP_BUBBLEUPNP = "3927FA74"
|
||||
const val APP_BBCSOUNDS = "03977A48"
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
package eu.toldi.balazs.caster
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
@ -14,6 +17,8 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.mapper.VideoInfo
|
||||
import eu.toldi.balazs.caster.model.ChromecastManageViewmodel
|
||||
import eu.toldi.balazs.caster.services.ChromecastManagerService
|
||||
import kotlinx.coroutines.withContext
|
||||
import su.litvak.chromecast.api.v2.Media
|
||||
import su.litvak.chromecast.api.v2.MediaStatus
|
||||
|
@ -37,69 +43,31 @@ import su.litvak.chromecast.api.v2.MediaStatus
|
|||
class ChromecastManagerActivity : ComponentActivity() {
|
||||
companion object {
|
||||
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 viewModel : ChromecastManageViewmodel
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (chromeCast_ == null)
|
||||
finish()
|
||||
chromeCast = chromeCast_ as ChromeCast
|
||||
ChromeCastHelper.chromeCast = chromeCast
|
||||
try {
|
||||
YoutubeDL.getInstance().init(application)
|
||||
} catch (e: YoutubeDLException) {
|
||||
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 {
|
||||
CasterTheme {
|
||||
val viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java)
|
||||
viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java)
|
||||
viewModel.chromeCast = chromeCast
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
|
@ -123,38 +91,53 @@ class ChromecastManagerActivity : ComponentActivity() {
|
|||
label = { Text("Cast URL") },
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
var castEnabled by remember { mutableStateOf(true)}
|
||||
var castEnabled by remember { mutableStateOf(true) }
|
||||
Button(
|
||||
enabled = castEnabled,
|
||||
onClick = {
|
||||
castEnabled = false
|
||||
lifecycleScope.launch {
|
||||
castLink(chromeCast, text) {
|
||||
viewModel.castLink(text) {
|
||||
castEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(text = "Cast")
|
||||
}
|
||||
if(castEnabled.not()){
|
||||
if (castEnabled.not()) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
playBackControl(viewmodel = viewModel)
|
||||
playBackControl()
|
||||
val mediaStatus = viewModel.mediaStatus.value
|
||||
|
||||
if(mediaStatus != null) {
|
||||
if (mediaStatus != null) {
|
||||
val nowPlaying = mediaStatus.media.metadata[Media.METADATA_TITLE]
|
||||
Text(text = "Now playing: $nowPlaying")
|
||||
val progress = mediaStatus.currentTime/ mediaStatus.media.duration
|
||||
Row(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()))
|
||||
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()
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = String.format(
|
||||
"%02d:%02d",
|
||||
((mediaStatus.media.duration % 3600) / 60).toInt(),
|
||||
(mediaStatus.media.duration % 60).toInt()
|
||||
)
|
||||
)
|
||||
}
|
||||
Slider(value = progress.toFloat(), onValueChange = {
|
||||
viewModel.seek(it.toDouble())
|
||||
Slider(value = sliderPosition, onValueChange = {
|
||||
sliderPosition = it
|
||||
}, onValueChangeFinished = {
|
||||
viewModel.seek(sliderPosition.toDouble())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -188,18 +171,15 @@ class ChromecastManagerActivity : ComponentActivity() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun playBackControl(viewmodel: ChromecastManageViewmodel){
|
||||
val mediaStatus = viewmodel.mediaStatus.value
|
||||
fun playBackControl() {
|
||||
val mediaStatus = viewModel.mediaStatus.value
|
||||
Row {
|
||||
|
||||
IconButton(onClick = {
|
||||
if(mediaStatus != null) {
|
||||
lifecycleScope.launch(IO) {
|
||||
chromeCast.seek(chromeCast.mediaStatus.currentTime - 10)
|
||||
viewmodel.fetchMediaStatus()
|
||||
if (mediaStatus != null) {
|
||||
viewModel.seek(chromeCast.mediaStatus.currentTime - 10)
|
||||
}
|
||||
}
|
||||
},enabled = mediaStatus != null) {
|
||||
}, enabled = mediaStatus != null) {
|
||||
Icon(
|
||||
Icons.Filled.FastRewind,
|
||||
contentDescription = "FastRewind"
|
||||
|
@ -212,18 +192,18 @@ class ChromecastManagerActivity : ComponentActivity() {
|
|||
playBackState = chromeCast.mediaStatus.playerState
|
||||
}*/
|
||||
IconButton(onClick = {
|
||||
lifecycleScope.launch(IO) {
|
||||
if(mediaStatus != null) {
|
||||
|
||||
if (mediaStatus != null) {
|
||||
if (mediaStatus.playerState == MediaStatus.PlayerState.PLAYING) {
|
||||
chromeCast.pause()
|
||||
viewModel.pause()
|
||||
}
|
||||
if (mediaStatus.playerState == MediaStatus.PlayerState.PAUSED) {
|
||||
chromeCast.play()
|
||||
viewModel.play()
|
||||
}
|
||||
viewmodel.fetchMediaStatus()
|
||||
viewModel.fetchMediaStatus()
|
||||
}
|
||||
}
|
||||
},enabled = mediaStatus != null) {
|
||||
|
||||
}, enabled = mediaStatus != null) {
|
||||
when {
|
||||
mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.PAUSED -> Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
|
@ -242,13 +222,8 @@ class ChromecastManagerActivity : ComponentActivity() {
|
|||
|
||||
}
|
||||
IconButton(onClick = {
|
||||
lifecycleScope.launch(IO) {
|
||||
if(chromeCast.runningApp != null) {
|
||||
chromeCast.stopApp()
|
||||
viewmodel.fetchMediaStatus()
|
||||
}
|
||||
}
|
||||
},enabled = mediaStatus != null) {
|
||||
viewModel.stopApp()
|
||||
}, enabled = mediaStatus != null) {
|
||||
Icon(
|
||||
Icons.Filled.Stop,
|
||||
contentDescription = "Stop"
|
||||
|
@ -256,13 +231,10 @@ class ChromecastManagerActivity : ComponentActivity() {
|
|||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
lifecycleScope.launch(IO) {
|
||||
if(chromeCast.mediaStatus != null) {
|
||||
chromeCast.seek(chromeCast.mediaStatus.currentTime + 10)
|
||||
viewmodel.fetchMediaStatus()
|
||||
if (chromeCast.mediaStatus != null) {
|
||||
viewModel.seek(chromeCast.mediaStatus.currentTime + 10)
|
||||
}
|
||||
}
|
||||
},enabled = mediaStatus != null) {
|
||||
}, enabled = mediaStatus != null) {
|
||||
Icon(
|
||||
Icons.Filled.FastForward,
|
||||
contentDescription = "FastForward"
|
||||
|
@ -302,21 +274,7 @@ class ChromecastManagerActivity : ComponentActivity() {
|
|||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
Button(
|
||||
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)
|
||||
}
|
||||
},
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(text = "Cast")
|
||||
|
@ -332,16 +290,14 @@ class ChromecastManagerActivity : ComponentActivity() {
|
|||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if(keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
GlobalScope.launch(IO) {
|
||||
chromeCast.setVolume(chromeCast.status.volume.level + 0.05f)
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
viewModel.setVolume(chromeCast.status.volume.level + 0.05f)
|
||||
return true
|
||||
}
|
||||
if(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
GlobalScope.launch(IO) {
|
||||
chromeCast.setVolume(chromeCast.status.volume.level - 0.05f)
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
|
||||
viewModel.setVolume(chromeCast.status.volume.level - 0.05f)
|
||||
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
|
|
|
@ -107,7 +107,8 @@ class ShareRecieverActivity : ComponentActivity() {
|
|||
onClick = {
|
||||
onEnableChanged(false)
|
||||
lifecycleScope.launch {
|
||||
ChromecastManagerActivity.castLink(chromeCast, link) {
|
||||
ChromeCastHelper.chromeCast = chromeCast
|
||||
ChromeCastHelper.castLink(link) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
10
app/src/main/res/drawable/forward.xml
Normal file
10
app/src/main/res/drawable/forward.xml
Normal 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>
|
10
app/src/main/res/drawable/pause.xml
Normal file
10
app/src/main/res/drawable/pause.xml
Normal 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>
|
10
app/src/main/res/drawable/play.xml
Normal file
10
app/src/main/res/drawable/play.xml
Normal 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>
|
10
app/src/main/res/drawable/rewind.xml
Normal file
10
app/src/main/res/drawable/rewind.xml
Normal 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>
|
|
@ -24,7 +24,7 @@ ext.getVersionCode = { ->
|
|||
try {
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'rev-list', '--first-parent', '--count', 'origin/main'
|
||||
commandLine 'git', 'rev-list', '--first-parent', '--count', '--all'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return Integer.parseInt(stdout.toString().trim())
|
||||
|
|
Loading…
Reference in a new issue