Compare commits
9 commits
40dec8832d
...
4c88992e5f
Author | SHA1 | Date | |
---|---|---|---|
4c88992e5f | |||
bebde9bc6a | |||
0432c0be90 | |||
cf4374d34b | |||
01b883ed27 | |||
dfb6cb969e | |||
f35295824c | |||
dcac6e7801 | |||
a4e760f9d1 |
17 changed files with 907 additions and 252 deletions
|
@ -29,8 +29,14 @@ steps:
|
|||
rm: true
|
||||
target: /home/fdroid-builder/unsigned
|
||||
source: "*.apk"
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
- name: update fdroid repo
|
||||
image: appleboy/drone-ssh
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
settings:
|
||||
host: git.toldi.eu
|
||||
username:
|
||||
|
|
|
@ -10,6 +10,27 @@
|
|||
<entry key="../../../../../layout/compose-model-1633601146222.xml" value="0.18285472972972974" />
|
||||
<entry key="../../../../../layout/compose-model-1633617799626.xml" value="0.3095439189189189" />
|
||||
<entry key="../../../../../layout/compose-model-1633617944748.xml" value="0.4962962962962963" />
|
||||
<entry key="../../../../../layout/compose-model-1633767363997.xml" value="0.17905405405405406" />
|
||||
<entry key="../../../../../layout/compose-model-1634889935306.xml" value="0.3095439189189189" />
|
||||
<entry key="../../../../../layout/compose-model-1634890564069.xml" value="0.4861111111111111" />
|
||||
<entry key="../../../../../layout/compose-model-1634891321942.xml" value="0.33" />
|
||||
<entry key="../../../../../layout/compose-model-1634891753898.xml" value="1.0" />
|
||||
<entry key="../../../../../layout/compose-model-1634893431951.xml" value="0.3095439189189189" />
|
||||
<entry key="../../../../../layout/compose-model-1634898066813.xml" value="0.3095439189189189" />
|
||||
<entry key="../../../../../layout/compose-model-1634898107086.xml" value="0.4759259259259259" />
|
||||
<entry key="../../../../../layout/compose-model-1634990577157.xml" value="0.4703703703703704" />
|
||||
<entry key="../../../../../layout/compose-model-1637651431048.xml" value="0.3611111111111111" />
|
||||
<entry key="../../../../../layout/compose-model-1637651867699.xml" value="0.3095439189189189" />
|
||||
<entry key="../../../../../layout/compose-model-1637652867578.xml" value="0.36018518518518516" />
|
||||
<entry key="../../../../../layout/compose-model-1637653004789.xml" value="0.45740740740740743" />
|
||||
<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" />
|
||||
<entry key="../../../../../layout/compose-model-1638180237267.xml" value="0.3095439189189189" />
|
||||
<entry key="../../../../../layout/compose-model-1638183069943.xml" value="0.33" />
|
||||
<entry key="../../../../../layout/compose-model-1638183903572.xml" value="0.48703703703703705" />
|
||||
</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,129 @@ 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 {
|
||||
try {
|
||||
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
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(null, e.stackTraceToString())
|
||||
} finally {
|
||||
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,89 +17,51 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.toldi.balazs.caster.ui.theme.CasterTheme
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import su.litvak.chromecast.api.v2.Application
|
||||
import su.litvak.chromecast.api.v2.ChromeCast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
|
||||
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 eu.toldi.balazs.caster.ui.theme.CasterTheme
|
||||
import su.litvak.chromecast.api.v2.ChromeCast
|
||||
import su.litvak.chromecast.api.v2.Media
|
||||
import su.litvak.chromecast.api.v2.MediaStatus
|
||||
|
||||
|
||||
class ChromecastManagerActivity : ComponentActivity() {
|
||||
companion object {
|
||||
var chromeCast_: ChromeCast? = null
|
||||
|
||||
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
|
||||
}
|
||||
GlobalScope.launch(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)
|
||||
}
|
||||
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 {
|
||||
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) {
|
||||
Column {
|
||||
//viewModel.fetchMediaStatus()
|
||||
MenuBar()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
@ -115,24 +80,61 @@ 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
|
||||
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()
|
||||
PlayBackControl()
|
||||
val mediaStatus = viewModel.mediaStatus.value
|
||||
|
||||
if (mediaStatus != null) {
|
||||
val nowPlaying =
|
||||
if (mediaStatus.media.metadata != null) mediaStatus.media.metadata[Media.METADATA_TITLE] else ""
|
||||
Text(text = "Now playing: $nowPlaying")
|
||||
var sliderPosition by remember { mutableStateOf(0.0f) }
|
||||
var sliderMoving by remember { mutableStateOf(false) }
|
||||
if (!sliderMoving)
|
||||
sliderPosition =
|
||||
(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 = sliderPosition, onValueChange = {
|
||||
sliderPosition = it
|
||||
sliderMoving = true
|
||||
}, onValueChangeFinished = {
|
||||
viewModel.seek(sliderPosition.toDouble())
|
||||
sliderMoving = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -164,63 +166,54 @@ class ChromecastManagerActivity : ComponentActivity() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun playBackControl(){
|
||||
fun PlayBackControl() {
|
||||
val mediaStatus = if (this::viewModel.isInitialized)
|
||||
viewModel.mediaStatus.value
|
||||
else null
|
||||
Row {
|
||||
|
||||
IconButton(onClick = {
|
||||
GlobalScope.launch(IO) {
|
||||
if(chromeCast.mediaStatus != null) {
|
||||
chromeCast.seek(chromeCast.mediaStatus.currentTime - 10)
|
||||
if (mediaStatus != null) {
|
||||
viewModel.seek(chromeCast.mediaStatus.currentTime - 10)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
}, enabled = mediaStatus != null) {
|
||||
Icon(
|
||||
Icons.Filled.FastRewind,
|
||||
contentDescription = "FastRewind"
|
||||
)
|
||||
}
|
||||
|
||||
var playBackState by remember { mutableStateOf(MediaStatus.PlayerState.IDLE) }
|
||||
GlobalScope.launch(IO) {
|
||||
if(chromeCast.mediaStatus != null)
|
||||
playBackState = chromeCast.mediaStatus.playerState
|
||||
}
|
||||
IconButton(onClick = {
|
||||
GlobalScope.launch(IO) {
|
||||
if(chromeCast.mediaStatus != null)
|
||||
playBackState = chromeCast.mediaStatus.playerState
|
||||
if(playBackState == MediaStatus.PlayerState.PLAYING) {
|
||||
chromeCast.pause()
|
||||
|
||||
if (mediaStatus != null) {
|
||||
if (mediaStatus.playerState == MediaStatus.PlayerState.PLAYING) {
|
||||
viewModel.pause()
|
||||
}
|
||||
if(playBackState == MediaStatus.PlayerState.PAUSED) {
|
||||
chromeCast.play()
|
||||
if (mediaStatus.playerState == MediaStatus.PlayerState.PAUSED) {
|
||||
viewModel.play()
|
||||
}
|
||||
if(chromeCast.mediaStatus != null)
|
||||
playBackState = chromeCast.mediaStatus.playerState
|
||||
}
|
||||
}) {
|
||||
when(playBackState) {
|
||||
MediaStatus.PlayerState.PLAYING -> Icon(
|
||||
Icons.Filled.Pause,
|
||||
contentDescription = "Pause"
|
||||
)
|
||||
MediaStatus.PlayerState.PAUSED -> Icon(
|
||||
|
||||
}, enabled = mediaStatus != null) {
|
||||
when {
|
||||
mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.PAUSED -> Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = "Resume"
|
||||
)
|
||||
mediaStatus.playerState == MediaStatus.PlayerState.PLAYING -> Icon(
|
||||
Icons.Filled.Pause,
|
||||
contentDescription = "Pause"
|
||||
)
|
||||
else -> Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = "Resume"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
IconButton(onClick = {
|
||||
GlobalScope.launch(IO) {
|
||||
if(chromeCast.runningApp != null)
|
||||
chromeCast.stopApp()
|
||||
}
|
||||
}) {
|
||||
viewModel.stopApp()
|
||||
}, enabled = mediaStatus != null) {
|
||||
Icon(
|
||||
Icons.Filled.Stop,
|
||||
contentDescription = "Stop"
|
||||
|
@ -228,12 +221,10 @@ class ChromecastManagerActivity : ComponentActivity() {
|
|||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
GlobalScope.launch(IO) {
|
||||
if(chromeCast.mediaStatus != null) {
|
||||
chromeCast.seek(chromeCast.mediaStatus.currentTime + 10)
|
||||
if (chromeCast.mediaStatus != null) {
|
||||
viewModel.seek(chromeCast.mediaStatus.currentTime + 10)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
}, enabled = mediaStatus != null) {
|
||||
Icon(
|
||||
Icons.Filled.FastForward,
|
||||
contentDescription = "FastForward"
|
||||
|
@ -273,42 +264,30 @@ 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")
|
||||
}
|
||||
playBackControl()
|
||||
// val viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java)
|
||||
PlayBackControl()
|
||||
Text(text = "Now playing: Some video")
|
||||
var sliderPosition by remember { mutableStateOf(0f) }
|
||||
Slider(value = sliderPosition, onValueChange = { sliderPosition = it })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -6,32 +6,22 @@ import android.os.Bundle
|
|||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
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.lifecycle.ViewModelProvider
|
||||
import eu.toldi.balazs.caster.model.ChromeCastViewModel
|
||||
import eu.toldi.balazs.caster.ui.theme.CasterTheme
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import su.litvak.chromecast.api.v2.Application
|
||||
import su.litvak.chromecast.api.v2.ChromeCast
|
||||
import su.litvak.chromecast.api.v2.ChromeCasts
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
|
@ -41,90 +31,144 @@ class MainActivity : ComponentActivity() {
|
|||
|
||||
setContent {
|
||||
CasterTheme {
|
||||
GlobalScope.launch(IO) {
|
||||
ChromeCasts.startDiscovery(getIPv4Address())
|
||||
}
|
||||
val model: ChromeCastViewModel by viewModels()
|
||||
var chromecastCount by remember {
|
||||
mutableStateOf(ChromeCasts.get().size)
|
||||
}
|
||||
val viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
|
||||
viewModel.startScanning()
|
||||
|
||||
val chromeCastState = viewModel.chromeCasts.observeAsState(initial = emptyList())
|
||||
val chromeCasts = chromeCastState.value
|
||||
Log.e(null, chromeCasts.toString())
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
Column {
|
||||
MenuBar()
|
||||
var isAddChromecastDialogOpen by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
MenuBar(
|
||||
refresh = {
|
||||
viewModel.refresh()
|
||||
},
|
||||
add = {
|
||||
isAddChromecastDialogOpen = true
|
||||
})
|
||||
|
||||
Text(text = "Chromecast count $chromecastCount")
|
||||
LazyColumn(modifier = Modifier
|
||||
|
||||
if (isAddChromecastDialogOpen) {
|
||||
showAddChromecastDialog(dismiss = {
|
||||
isAddChromecastDialogOpen = false
|
||||
}) {
|
||||
if (it.matches(Regex("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\$")))
|
||||
viewModel.addChromecast(ChromeCast(it).also { chromeCast ->
|
||||
chromeCast.name = "Chromecast@$it"
|
||||
})
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(all = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
if (chromecastCount > 0)
|
||||
if (chromeCasts.isNotEmpty())
|
||||
Text(text = "Available chromecasts:")
|
||||
}
|
||||
items(chromecastCount) { index ->
|
||||
showChromeCastButton(chromeCast = ChromeCasts.get()[index])
|
||||
else {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("Looking for Chromecasts on your network...")
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SideEffect {
|
||||
GlobalScope.launch(IO) {
|
||||
while (true) {
|
||||
chromecastCount = ChromeCasts.get().size
|
||||
delay(1000)
|
||||
items(chromeCasts.size) { index ->
|
||||
showChromeCastButton(chromeCast = chromeCasts[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun showAddChromecastDialog(dismiss: () -> Unit, add: (String) -> Unit) {
|
||||
var ipaddress by remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
AlertDialog(onDismissRequest = dismiss,
|
||||
title = {
|
||||
Text(text = "Add Chromecast")
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = ipaddress,
|
||||
onValueChange = {
|
||||
ipaddress = it
|
||||
},
|
||||
label = { Text("IP address of the chromecast") },
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
}, buttons = {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 8.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
add(ipaddress)
|
||||
dismiss()
|
||||
},
|
||||
modifier = Modifier.padding(all = 8.dp)
|
||||
) {
|
||||
Text(text = "Add Chromecast")
|
||||
}
|
||||
Button(onClick = dismiss, modifier = Modifier.padding(all = 8.dp)) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun showChromeCastButton(chromeCast: ChromeCast) {
|
||||
Button(onClick = {
|
||||
Button(
|
||||
onClick = {
|
||||
ChromecastManagerActivity.chromeCast_ = chromeCast
|
||||
val intent = Intent(this, ChromecastManagerActivity::class.java)
|
||||
startActivity(intent)
|
||||
},
|
||||
modifier = Modifier.padding(5.dp)
|
||||
) {
|
||||
Text(text = chromeCast.model)
|
||||
}
|
||||
when {
|
||||
chromeCast.title != null -> Text(text = chromeCast.title)
|
||||
chromeCast.name != null -> Text(text = chromeCast.name)
|
||||
else -> Text(text = chromeCast.address)
|
||||
}
|
||||
|
||||
fun getIPv4Address(): InetAddress? {
|
||||
NetworkInterface.getNetworkInterfaces().toList().forEach { interf ->
|
||||
interf.inetAddresses.toList().forEach { inetAddress ->
|
||||
if (!inetAddress.isLoopbackAddress && inetAddress.hostAddress.indexOf(':') < 0) {
|
||||
return inetAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MenuBar() {
|
||||
fun MenuBar(refresh: () -> Unit, add: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text("Caster")
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
Icons.Filled.Menu,
|
||||
contentDescription = "Menu Hamburger"
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
Row {
|
||||
IconButton(onClick = {}) {
|
||||
IconButton(onClick = refresh) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = "Search Article"
|
||||
Icons.Filled.Refresh,
|
||||
contentDescription = "Refresh"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = add) {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
contentDescription = "Add"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +181,7 @@ class MainActivity : ComponentActivity() {
|
|||
@Composable
|
||||
fun DefaultPreview() {
|
||||
CasterTheme {
|
||||
MenuBar()
|
||||
MenuBar({}, {})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,17 +13,19 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import com.yausername.youtubedl_android.YoutubeDLException
|
||||
import eu.toldi.balazs.caster.model.ChromeCastViewModel
|
||||
import eu.toldi.balazs.caster.ui.theme.CasterTheme
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
|
@ -50,15 +52,16 @@ class ShareRecieverActivity : ComponentActivity() {
|
|||
}
|
||||
setContent {
|
||||
CasterTheme {
|
||||
var chromecastCount by remember { mutableStateOf(ChromeCasts.get().size) }
|
||||
GlobalScope.launch(IO) {
|
||||
ChromeCasts.startDiscovery(getIPv4Address())
|
||||
}
|
||||
Log.i(null, "Chromecast count: $chromecastCount")
|
||||
val viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
|
||||
viewModel.startScanning()
|
||||
val chromeCastState = viewModel.chromeCasts.observeAsState(emptyList())
|
||||
val chromeCasts = chromeCastState.value
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
Column {
|
||||
MenuBar()
|
||||
MenuBar{
|
||||
viewModel.refresh()
|
||||
}
|
||||
var enabled by remember {
|
||||
mutableStateOf(true)
|
||||
}
|
||||
|
@ -69,14 +72,14 @@ class ShareRecieverActivity : ComponentActivity() {
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
if (chromecastCount > 0)
|
||||
if (chromeCasts.isNotEmpty())
|
||||
Text(text = "Available chromecasts:")
|
||||
}
|
||||
items(chromecastCount) { index ->
|
||||
items(chromeCasts.size) { index ->
|
||||
showChromeCastButton(
|
||||
enabled = enabled,
|
||||
onEnableChanged = { enabled = it },
|
||||
chromeCast = ChromeCasts.get()[index],
|
||||
chromeCast = chromeCasts[index],
|
||||
link = link
|
||||
)
|
||||
}
|
||||
|
@ -87,12 +90,6 @@ class ShareRecieverActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(true) {
|
||||
while (true) {
|
||||
delay(2000)
|
||||
chromecastCount = ChromeCasts.get().size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,9 +106,12 @@ class ShareRecieverActivity : ComponentActivity() {
|
|||
Button(
|
||||
onClick = {
|
||||
onEnableChanged(false)
|
||||
ChromecastManagerActivity.castLink(chromeCast, link) {
|
||||
lifecycleScope.launch {
|
||||
ChromeCastHelper.chromeCast = chromeCast
|
||||
ChromeCastHelper.castLink(link) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(5.dp),
|
||||
enabled = enabled
|
||||
|
@ -120,37 +120,27 @@ class ShareRecieverActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
fun getIPv4Address(): InetAddress? {
|
||||
NetworkInterface.getNetworkInterfaces().toList().forEach { interf ->
|
||||
interf.inetAddresses.toList().forEach { inetAddress ->
|
||||
if (!inetAddress.isLoopbackAddress && inetAddress.hostAddress.indexOf(':') < 0) {
|
||||
return inetAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MenuBar() {
|
||||
fun MenuBar(refresh: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(id = R.string.title_activity_share_reciever))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { }) {
|
||||
Icon(
|
||||
Icons.Filled.Menu,
|
||||
contentDescription = "Menu Hamburger"
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
Row {
|
||||
IconButton(onClick = {}) {
|
||||
IconButton(onClick = refresh) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = "Search Article"
|
||||
Icons.Filled.Refresh,
|
||||
contentDescription = "Refresh"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
contentDescription = "Add"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +153,7 @@ class ShareRecieverActivity : ComponentActivity() {
|
|||
@Composable
|
||||
fun DefaultPreview() {
|
||||
CasterTheme {
|
||||
MenuBar()
|
||||
MenuBar {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +1,67 @@
|
|||
package eu.toldi.balazs.caster.model
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import su.litvak.chromecast.api.v2.ChromeCast
|
||||
import su.litvak.chromecast.api.v2.ChromeCasts
|
||||
import su.litvak.chromecast.api.v2.ChromeCastsListener
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
|
||||
class ChromeCastViewModel : ViewModel() {
|
||||
private val chromeCasts : MutableLiveData<List<ChromeCast>> by lazy {
|
||||
MutableLiveData<List<ChromeCast>>().also {
|
||||
ChromeCasts.get()
|
||||
class ChromeCastViewModel : ViewModel(),ChromeCastsListener {
|
||||
private val _chromecasts: MutableLiveData<List<ChromeCast>> =
|
||||
MutableLiveData<List<ChromeCast>>(listOf())
|
||||
val chromeCasts: LiveData<List<ChromeCast>>
|
||||
get() = _chromecasts
|
||||
|
||||
init {
|
||||
ChromeCasts.registerListener(this)
|
||||
}
|
||||
|
||||
fun addChromecast(chromeCast: ChromeCast) {
|
||||
_chromecasts.postValue(_chromecasts.value!!.plus(chromeCast))
|
||||
}
|
||||
|
||||
override fun newChromeCastDiscovered(chromeCast: ChromeCast?) {
|
||||
if (chromeCast != null) {
|
||||
Log.i(null, "Found ${chromeCast.title}")
|
||||
_chromecasts.postValue(_chromecasts.value!!.plus(chromeCast))
|
||||
}
|
||||
}
|
||||
|
||||
fun getChromecasts() : LiveData<List<ChromeCast>> {
|
||||
return chromeCasts
|
||||
override fun chromeCastRemoved(chromeCast: ChromeCast?) {
|
||||
if (chromeCast != null) {
|
||||
Log.i(null,"Lost ${chromeCast.title}")
|
||||
_chromecasts.postValue(_chromecasts.value!!.minus(chromeCast))
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
_chromecasts.value = emptyList()
|
||||
viewModelScope.launch(IO) {
|
||||
ChromeCasts.restartDiscovery(getIPv4Address())
|
||||
}
|
||||
}
|
||||
|
||||
fun startScanning() {
|
||||
viewModelScope.launch(IO) {
|
||||
ChromeCasts.startDiscovery(getIPv4Address())
|
||||
}
|
||||
}
|
||||
fun getIPv4Address(): InetAddress? {
|
||||
NetworkInterface.getNetworkInterfaces().toList().forEach { interf ->
|
||||
interf.inetAddresses.toList().forEach { inetAddress ->
|
||||
if (!inetAddress.isLoopbackAddress && inetAddress.hostAddress.indexOf(':') < 0) {
|
||||
return inetAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
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.MediaStatus
|
||||
|
||||
class ChromecastManageViewmodel : ViewModel() {
|
||||
|
||||
private val _mediaState = mutableStateOf<MediaStatus?>(null)
|
||||
val mediaStatus: State<MediaStatus?>
|
||||
get() = _mediaState
|
||||
lateinit var chromeCast: ChromeCast
|
||||
|
||||
|
||||
init {
|
||||
viewModelScope.launch(IO) {
|
||||
fetchMediaStatus()
|
||||
while (true) {
|
||||
fetchMediaStatus()
|
||||
Thread.sleep(3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
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) {
|
||||
try {
|
||||
_mediaState.value = ChromeCastHelper.fetchMediaStatus()
|
||||
} catch (e: Exception) {
|
||||
Log.e(null, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,274 @@
|
|||
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.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.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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(10500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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