Compare commits

...

10 commits

Author SHA1 Message Date
8865f2c5b4
Added intent filter for video/image files
Signed-off-by: Balazs Toldi <balazs@toldi.eu>
2022-01-03 23:30:55 +01:00
5933cd2b8a
Fixed image loading
Signed-off-by: Balazs Toldi <balazs@toldi.eu>
2022-01-03 11:23:02 +01:00
55ca983cf7
Redesigned Main Activity
Signed-off-by: Balazs Toldi <balazs@toldi.eu>
2022-01-02 22:17:46 +01:00
f681b7afc9
Load images for each video
Signed-off-by: Balazs Toldi <balazs@toldi.eu>
2022-01-02 21:38:24 +01:00
a55f2bcd44
Minor bug fixes
Signed-off-by: Balazs Toldi <balazs@toldi.eu>
2022-01-02 21:13:38 +01:00
5672eab2bd
Minor changes
Some checks reported errors
continuous-integration/drone/push Build was killed
Signed-off-by: Toldi Balázs Ádám <balazs@toldi.eu>
2021-12-05 23:19:01 +01:00
0943680271 Moved strings to strings.xml 2021-12-05 20:43:13 +01:00
4c88992e5f
Functional add button for the main activity
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: Balazs Toldi <balazs@toldi.eu>
2021-11-29 12:52:38 +01:00
bebde9bc6a
Functional add button for the main activity
Signed-off-by: Balazs Toldi <balazs@toldi.eu>
2021-11-29 12:51:44 +01:00
0432c0be90
Stability changes
Signed-off-by: Balazs Toldi <balazs@toldi.eu>
2021-11-29 12:04:52 +01:00
26 changed files with 850 additions and 224 deletions

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_3a_XL_API_30.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-12-05T17:39:35.648801Z" />
<targetsSelectedWithDialog>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_3a_XL_API_30.avd" />
</Key>
</deviceKey>
</Target>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Android_TV_720p_API_30.avd" />
</Key>
</deviceKey>
</Target>
</targetsSelectedWithDialog>
</component>
</project>

View file

@ -28,6 +28,29 @@
<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" />
<entry key="../../../../../layout/compose-model-1638742625644.xml" value="0.48055555555555557" />
<entry key="../../../../../layout/compose-model-1638742825833.xml" value="0.1" />
<entry key="../../../../../layout/compose-model-1639485191192.xml" value="0.1" />
<entry key="../../../../../layout/compose-model-1641150442256.xml" value="0.4564814814814815" />
<entry key="../../../../../layout/compose-model-1641153447766.xml" value="0.33" />
<entry key="../../../../../layout/compose-model-1641153474417.xml" value="0.4537037037037037" />
<entry key="../../../../../layout/compose-model-1641154461708.xml" value="0.30743243243243246" />
<entry key="../../../../../layout/compose-model-1641155483959.xml" value="0.25" />
<entry key="../../../../../layout/compose-model-1641156419059.xml" value="0.5" />
<entry key="../../../../../layout/compose-model-1641157410658.xml" value="1.0" />
<entry key="../../../../../layout/compose-model-1641157605359.xml" value="0.5" />
<entry key="../../../../../layout/compose-model-1641158311201.xml" value="1.0" />
<entry key="../../../../../layout/compose-model-1641198484109.xml" value="0.30743243243243246" />
<entry key="../../../../../layout/compose-model-1641204990382.xml" value="0.1" />
<entry key="../../../../../layout/compose-model-1641204995500.xml" value="0.1" />
<entry key="../../../../../layout/compose-model-1641204997625.xml" value="0.44814814814814813" />
<entry key="../../../../../layout/compose-model-1641219750398.xml" value="0.25" />
<entry key="../../../../../layout/compose-model-1641222972102.xml" value="0.46296296296296297" />
<entry key="../../../../../layout/compose-model-1641224248444.xml" value="0.46296296296296297" />
<entry key="../../../../../layout/compose-model-1641226030069.xml" value="0.46296296296296297" />
</map>
</option>
</component>

View file

@ -4,6 +4,8 @@ plugins {
}
android {
compileSdk 31
defaultConfig {
@ -17,6 +19,11 @@ android {
vectorDrawables {
useSupportLibrary true
}
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
buildTypes {
@ -29,6 +36,16 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
splits {
abi {
enable true
reset()
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
universalApk true
}
}
kotlinOptions {
jvmTarget = '1.8'
useIR = true
@ -41,6 +58,10 @@ android {
kotlinCompilerVersion '1.5.21'
}
packagingOptions {
exclude 'META-INF/*'
// Due to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
exclude 'META-INF/licenses/*'
exclude '**/attach_hotspot_windows.dll'
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
@ -62,9 +83,15 @@ dependencies {
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("io.coil-kt:coil-compose:1.4.0")
implementation "com.google.accompanist:accompanist-swiperefresh:0.22.0-rc"
implementation 'com.github.yausername.youtubedl-android:library:0.13.+'
implementation 'com.github.yausername.youtubedl-android:ffmpeg:0.13.+'
implementation "androidx.media:media:1.4.3"
implementation 'io.ktor:ktor-server-core:1.6.2'
implementation 'io.ktor:ktor-server-jetty:1.6.2'
implementation 'io.ktor:ktor-websockets:1.6.2'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View file

@ -3,8 +3,9 @@
package="eu.toldi.balazs.caster">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name=".App"
@ -13,6 +14,8 @@
android:label="@string/app_name"
android:roundIcon="@drawable/ic_caster_logo"
android:supportsRtl="true"
android:extractNativeLibs="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/Theme.Caster">
<activity
android:name=".ShareRecieverActivity"
@ -24,6 +27,16 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<activity
android:name=".ChromecastManagerActivity"

View file

@ -11,6 +11,8 @@ 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
import java.net.InetAddress
import java.net.NetworkInterface
object ChromeCastHelper {
@ -25,6 +27,7 @@ object ChromeCastHelper {
const val APP_BUBBLEUPNP = "3927FA74"
const val APP_BBCSOUNDS = "03977A48"
const val APP_BBCIPLAYER = "5E81F6DB"
const val APP_YOUTUBE_MUSIC = "2DB7CC49"
lateinit var chromeCast: ChromeCast
@ -32,6 +35,7 @@ object ChromeCastHelper {
return when (id) {
APP_MEDIA_RECEIVER -> "Media Player"
APP_YOUTUBE -> "Youtube"
APP_YOUTUBE_MUSIC -> "Youtube Music"
APP_PLEX -> "Plex"
APP_BACKDROP -> "Idle"
APP_DASHCAST -> "Dashcast"
@ -66,7 +70,11 @@ object ChromeCastHelper {
if (mediaStatus != null) {
try {
withContext(IO) {
try {
chromeCast.seek(d * mediaStatus.media.duration)
}catch (e:Exception) {
Log.e(null,e.stackTraceToString())
}
}
} catch (e: Exception) {
Log.e(null, e.stackTraceToString())
@ -96,7 +104,13 @@ object ChromeCastHelper {
suspend fun setVolume(f: Float) {
withContext(IO) {
if(f >= 0.0f && f <= 1.0f) {
try {
chromeCast.setVolume(f)
}catch (e:Exception) {
Log.e(null,e.stackTraceToString())
}
}
}
}
@ -120,14 +134,15 @@ object ChromeCastHelper {
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)
val app: Application =
chromeCast.launchApp(ChromeCastHelper.APP_MEDIA_RECEIVER)
}
while (!chromeCast.status.isAppRunning(ChromeCastHelper.APP_MEDIA_RECEIVER)) {
delay(100)
@ -139,11 +154,23 @@ object ChromeCastHelper {
streamInfo.url,
null
)
} catch (e: Exception) {
Log.e(null, e.stackTraceToString())
} finally {
callBack.invoke()
}
}
}
return exitStatus
}
fun getIPv4Address(): InetAddress? {
NetworkInterface.getNetworkInterfaces().toList().forEach { interf ->
interf.inetAddresses.toList().forEach { inetAddress ->
if (!inetAddress.isLoopbackAddress && inetAddress.hostAddress.indexOf(':') < 0) {
return inetAddress
}
}
}
return null
}
}

View file

@ -1,13 +1,21 @@
package eu.toldi.balazs.caster
import android.annotation.SuppressLint
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
import android.util.Log
import android.view.KeyEvent
import android.webkit.MimeTypeMap
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@ -15,34 +23,31 @@ import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
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.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
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 coil.compose.rememberImagePainter
import com.yausername.ffmpeg.FFmpeg
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.helpers.FileCacheHelper
import eu.toldi.balazs.caster.model.ChromecastManageViewmodel
import eu.toldi.balazs.caster.services.ChromecastManagerService
import kotlinx.coroutines.withContext
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
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.FileOutputStream
class ChromecastManagerActivity : ComponentActivity() {
companion object {
var chromeCast_: ChromeCast? = null
const val METADATA_THUMB = "thumb"
}
private lateinit var chromeCast: ChromeCast
@ -56,10 +61,11 @@ class ChromecastManagerActivity : ComponentActivity() {
ChromeCastHelper.chromeCast = chromeCast
try {
YoutubeDL.getInstance().init(application)
FFmpeg.getInstance().init(application)
} catch (e: YoutubeDLException) {
Log.e("Caster", "failed to initialize youtubedl-android", e)
}
val serviceIntent = Intent(this, ChromecastManagerService::class.java).also {
Intent(this, ChromecastManagerService::class.java).also {
it.action = ChromecastManagerService.ACTION_INIT
it.putExtra("CHROMECAST_ADDRESS", chromeCast.address)
it.putExtra("CHROMECAST_NAME", chromeCast.title)
@ -72,7 +78,7 @@ class ChromecastManagerActivity : ComponentActivity() {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Column {
viewModel.fetchMediaStatus()
//viewModel.fetchMediaStatus()
MenuBar()
Column(
modifier = Modifier
@ -88,7 +94,7 @@ class ChromecastManagerActivity : ComponentActivity() {
onValueChange = {
text = it
},
label = { Text("Cast URL") },
label = { Text(stringResource(id = R.string.cast_url)) },
modifier = Modifier.padding(vertical = 4.dp)
)
var castEnabled by remember { mutableStateOf(true) }
@ -102,19 +108,62 @@ class ChromecastManagerActivity : ComponentActivity() {
},
modifier = Modifier.padding(vertical = 4.dp)
) {
Text(text = "Cast")
Text(text = stringResource(id = R.string.cast))
}
if (castEnabled.not()) {
CircularProgressIndicator()
}
playBackControl()
PlayBackControl()
mediaStatus()
}
}
}
}
}
}
@Composable
fun mediaStatus() {
val mediaStatus = viewModel.mediaStatus.value
if (mediaStatus != null) {
val nowPlaying = mediaStatus.media.metadata[Media.METADATA_TITLE]
Text(text = "Now playing: $nowPlaying")
var sliderPosition by remember { mutableStateOf((mediaStatus.currentTime / mediaStatus.media.duration).toFloat()) }
val nowPlaying =
if (mediaStatus.media.metadata != null) mediaStatus.media.metadata[Media.METADATA_TITLE] else ""
Text(text = stringResource(id = R.string.now_playing) + ": $nowPlaying")
var sliderPosition by remember { mutableStateOf(0.0f) }
var sliderMoving by remember { mutableStateOf(false) }
if (mediaStatus.media.duration != null) {
if (!sliderMoving)
sliderPosition =
(mediaStatus.currentTime / mediaStatus.media.duration).toFloat()
}
if (mediaStatus.media.metadata[Media.METADATA_IMAGES] != null) {
val images =
mediaStatus.media.metadata[Media.METADATA_IMAGES] as ArrayList<LinkedHashMap<String, String>>
if (images.size >= 1 && images[0].containsKey("url")) {
Log.e("Caster", images[0]["url"].toString())
Image(
painter = rememberImagePainter(images[0]["url"]),
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
}
} else if (mediaStatus.media.metadata[METADATA_THUMB] != null) {
val image =
mediaStatus.media.metadata[METADATA_THUMB] as String
Log.e("Caster", image)
Image(
painter = rememberImagePainter(image),
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
}
if(mediaStatus.media.contentType.startsWith("video/")) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
@ -136,20 +185,28 @@ class ChromecastManagerActivity : ComponentActivity() {
}
Slider(value = sliderPosition, onValueChange = {
sliderPosition = it
sliderMoving = true
}, onValueChangeFinished = {
viewModel.seek(sliderPosition.toDouble())
sliderMoving = false
})
}
}
}
}
}
}
}
@Composable
fun MenuBar() {
val pickPictureLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument()
) { fileUri ->
if (fileUri != null) {
val cacheHelper = FileCacheHelper(applicationContext)
cacheHelper.cacheThis(listOf(fileUri))
viewModel.castFromCache(cacheHelper.tryFileName(fileUri))
}
}
TopAppBar(
title = {
Text("Caster")
@ -158,39 +215,42 @@ class ChromecastManagerActivity : ComponentActivity() {
IconButton(onClick = { finish() }) {
Icon(
Icons.Filled.ArrowBack,
contentDescription = "Back"
contentDescription = stringResource(id = R.string.back)
)
}
},
actions = {
Row {
IconButton(onClick = {
pickPictureLauncher.launch(arrayOf("image/*", "video/*"))
}) {
Icon(
Icons.Filled.Folder,
contentDescription = stringResource(id = R.string.back)
)
}
}
}
)
}
@Composable
fun playBackControl() {
val mediaStatus = viewModel.mediaStatus.value
fun PlayBackControl() {
val mediaStatus = if (this::viewModel.isInitialized)
viewModel.mediaStatus.value
else null
Row {
IconButton(onClick = {
if (mediaStatus != null) {
viewModel.seek(chromeCast.mediaStatus.currentTime - 10)
viewModel.seek(mediaStatus.currentTime - 10)
}
}, enabled = mediaStatus != null) {
Icon(
Icons.Filled.FastRewind,
contentDescription = "FastRewind"
contentDescription = stringResource(id = R.string.rewind)
)
}
/*
var playBackState by remember { mutableStateOf(MediaStatus.PlayerState.IDLE) }
lifecycleScope.launch(IO) {
if(chromeCast.mediaStatus != null)
playBackState = chromeCast.mediaStatus.playerState
}*/
IconButton(onClick = {
if (mediaStatus != null) {
@ -200,22 +260,21 @@ class ChromecastManagerActivity : ComponentActivity() {
if (mediaStatus.playerState == MediaStatus.PlayerState.PAUSED) {
viewModel.play()
}
viewModel.fetchMediaStatus()
}
}, enabled = mediaStatus != null) {
when {
mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.PAUSED -> Icon(
Icons.Filled.PlayArrow,
contentDescription = "Resume"
contentDescription = stringResource(id = R.string.resume)
)
mediaStatus.playerState == MediaStatus.PlayerState.PLAYING -> Icon(
Icons.Filled.Pause,
contentDescription = "Pause"
contentDescription = stringResource(id = R.string.pause)
)
else -> Icon(
Icons.Filled.PlayArrow,
contentDescription = "Resume"
contentDescription = stringResource(id = R.string.resume)
)
}
@ -226,18 +285,18 @@ class ChromecastManagerActivity : ComponentActivity() {
}, enabled = mediaStatus != null) {
Icon(
Icons.Filled.Stop,
contentDescription = "Stop"
contentDescription = stringResource(id = R.string.stop)
)
}
IconButton(onClick = {
if (chromeCast.mediaStatus != null) {
viewModel.seek(chromeCast.mediaStatus.currentTime + 10)
if (mediaStatus != null) {
viewModel.seek(mediaStatus.currentTime + 10)
}
}, enabled = mediaStatus != null) {
Icon(
Icons.Filled.FastForward,
contentDescription = "FastForward"
contentDescription = stringResource(id = R.string.fastforward)
)
}
}
@ -270,18 +329,18 @@ class ChromecastManagerActivity : ComponentActivity() {
onValueChange = {
text = it
},
label = { Text("Cast URL") },
label = { Text(stringResource(id = R.string.cast_url)) },
modifier = Modifier.padding(vertical = 4.dp)
)
Button(
onClick = {},
modifier = Modifier.padding(vertical = 4.dp)
) {
Text(text = "Cast")
Text(text = stringResource(id = R.string.cast))
}
// val viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java)
//playBackControl(viewModel)
Text(text = "Now playing: Some video")
PlayBackControl()
Text(text = stringResource(id = R.string.now_playing) + ": Some video")
var sliderPosition by remember { mutableStateOf(0f) }
Slider(value = sliderPosition, onValueChange = { sliderPosition = it })
}
@ -291,12 +350,12 @@ class ChromecastManagerActivity : ComponentActivity() {
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
viewModel.setVolume(chromeCast.status.volume.level + 0.05f)
viewModel.increaseVolume()
return true
}
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
viewModel.setVolume(chromeCast.status.volume.level - 0.05f)
viewModel.decreaseVolume()
return true
}

View file

@ -2,36 +2,64 @@ package eu.toldi.balazs.caster
import android.annotation.SuppressLint
import android.content.Intent
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.MulticastLock
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.Image
import androidx.compose.foundation.clickable
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.*
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.res.painterResource
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.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
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() {
open class MainActivity : ComponentActivity() {
protected lateinit var viewModel: ChromeCastViewModel
protected var multicastLock: MulticastLock? = null
protected fun isViewModelInitialised() = ::viewModel.isInitialized
override fun onStart() {
super.onStart()
val wifi = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
multicastLock = wifi.createMulticastLock(javaClass.name)
multicastLock!!.setReferenceCounted(true)
multicastLock!!.acquire()
if (!this::viewModel.isInitialized)
viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
viewModel.startScanning()
}
override fun onStop() {
super.onStop()
if (multicastLock != null) {
Log.i("Caster", "Releasing Mutlicast Lock...")
multicastLock!!.release()
multicastLock = null
}
viewModel.startScanning()
}
@SuppressLint("CoroutineCreationDuringComposition")
override fun onCreate(savedInstanceState: Bundle?) {
@ -39,31 +67,69 @@ class MainActivity : ComponentActivity() {
setContent {
CasterTheme {
val viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
viewModel.startScanning()
viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
val chromeCastState = viewModel.chromeCasts.observeAsState(initial = emptyList())
val chromeCasts = chromeCastState.value
Log.e(null,chromeCasts.toString())
Log.e(null, chromeCasts.toString())
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Column {
MenuBar {
viewModel.refresh()
Column(modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()) {
var isAddChromecastDialogOpen by remember {
mutableStateOf(false)
}
LazyColumn(modifier = Modifier
MenuBar(
refresh = {
viewModel.refresh()
},
add = {
isAddChromecastDialogOpen = true
})
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"
})
}
}
var isRefreshing by remember {
mutableStateOf(false)
}
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = {
isRefreshing = true
viewModel.refresh()
isRefreshing = false
},
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
LazyColumn(
modifier = Modifier
.padding(all = 4.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally) {
.fillMaxWidth()
.fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
if (chromeCasts.isNotEmpty())
Text(text = "Available chromecasts:")
Text(text = stringResource(id = R.string.available_chromecasts))
else {
Column(
modifier = Modifier.fillMaxSize() ,
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
){
Text("Looking for Chromecasts on your network...")
) {
Text(stringResource(id = R.string.looking_for_devices))
CircularProgressIndicator()
}
}
@ -77,23 +143,108 @@ class MainActivity : ComponentActivity() {
}
}
}
}
@Composable
fun showChromeCastButton(chromeCast: ChromeCast) {
Button(onClick = {
ChromecastManagerActivity.chromeCast_ = chromeCast
val intent = Intent(this, ChromecastManagerActivity::class.java)
startActivity(intent)
fun showAddChromecastDialog(dismiss: () -> Unit, add: (String) -> Unit) {
var ipaddress by remember {
mutableStateOf("")
}
AlertDialog(onDismissRequest = dismiss,
title = {
Text(text = stringResource(id = R.string.add_chromecast))
},
modifier = Modifier.padding(5.dp)
text = {
OutlinedTextField(
value = ipaddress,
onValueChange = {
ipaddress = it
},
label = { Text(stringResource(id = R.string.ip_address)) },
modifier = Modifier.padding(vertical = 4.dp)
)
}, buttons = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
Text(text = chromeCast.model)
Button(
onClick = {
add(ipaddress)
dismiss()
},
modifier = Modifier.padding(all = 8.dp)
) {
Text(text = stringResource(id = R.string.add_chromecast))
}
Button(onClick = dismiss, modifier = Modifier.padding(all = 8.dp)) {
Text(stringResource(id = R.string.cancel))
}
}
})
}
@Composable
fun showChromeCastButton(chromeCast: ChromeCast,buttonCallBack: () -> Unit = {
ChromecastManagerActivity.chromeCast_ = chromeCast
val intent = Intent(applicationContext, ChromecastManagerActivity::class.java)
startActivity(intent)
}) {
Column(
Modifier
.fillMaxWidth()
.padding(all = 5.dp)
) {
Card(modifier = Modifier
.fillMaxWidth()
.clickable {
buttonCallBack()
}) {
Row {
val image_id = when (chromeCast.model) {
"Chromecast Ultra" -> R.drawable.chromecastultra
else -> R.drawable.chromecastv1
}
Column(
modifier = Modifier
.height(80.dp)
.width(80.dp)
.padding(10.dp)
) {
Image(
painter = painterResource(id = image_id),
contentDescription = chromeCast.model,
Modifier
.fillMaxHeight()
.fillMaxWidth()
)
}
Column(modifier = Modifier.fillMaxWidth().height(80.dp),
verticalArrangement = Arrangement.Center) {
Text(
text = "Name: " + when {
chromeCast.title != null -> chromeCast.title
chromeCast.name != null -> chromeCast.name
else -> chromeCast.address
}
)
Text(
text = "Model: " + when {
chromeCast.model != null -> chromeCast.model
else -> "Unknown"
}
)
}
}
}
}
}
@Composable
fun MenuBar(refresh: () -> Unit) {
fun MenuBar(refresh: () -> Unit, add: () -> Unit) {
TopAppBar(
title = {
Text("Caster")
@ -103,16 +254,14 @@ class MainActivity : ComponentActivity() {
IconButton(onClick = refresh) {
Icon(
Icons.Filled.Refresh,
contentDescription = "Refresh"
contentDescription = stringResource(id = R.string.refresh)
)
}
IconButton(onClick = {
}) {
IconButton(onClick = add) {
Icon(
Icons.Filled.Add,
contentDescription = "Add"
contentDescription = stringResource(id = R.string.add_chromecast)
)
}
}
@ -123,9 +272,26 @@ class MainActivity : ComponentActivity() {
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
open fun DefaultPreview() {
CasterTheme {
MenuBar({})
Column {
MenuBar({}, {})
Column(
modifier = Modifier
.padding(all = 4.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
showChromeCastButton(
chromeCast = ChromeCast("127.0.0.1").apply {
name = "Chromecast@127.0.0.1"
}
)
}
}
}
}
}

View file

@ -2,18 +2,18 @@ package eu.toldi.balazs.caster
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Bundle
import android.os.Parcelable
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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.*
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
@ -21,30 +21,47 @@ 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.core.content.ContextCompat
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.helpers.FileCacheHelper
import eu.toldi.balazs.caster.model.ChromeCastViewModel
import eu.toldi.balazs.caster.services.ChromecastManagerService
import eu.toldi.balazs.caster.ui.theme.CasterTheme
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import su.litvak.chromecast.api.v2.Application
import kotlinx.coroutines.launch
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 ShareRecieverActivity : ComponentActivity() {
class ShareRecieverActivity : MainActivity() {
@SuppressLint("CoroutineCreationDuringComposition")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent?.action != Intent.ACTION_SEND || intent.type != "text/plain") {
if (intent?.action != Intent.ACTION_SEND ) {
finish()
}
val link = intent.getStringExtra(Intent.EXTRA_TEXT) as String
Log.d(null, intent.type.toString())
val link = when {
intent.type == "text/plain" -> intent.getStringExtra(Intent.EXTRA_TEXT) as String
intent.type?.startsWith("video/") == true ||
intent.type?.startsWith("image/") == true -> {
val uri = (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)
if(uri is Uri) {
val fileCache = FileCacheHelper(applicationContext)
fileCache.cacheThis(listOf(uri))
"http://" + ChromeCastHelper.getIPv4Address()?.hostAddress + ":" + ChromecastManagerService.PORT + "/assets/" + fileCache.tryFileName(
uri
)
}else ""
}
else -> {
""
}
}
if (link == "") finish()
try {
YoutubeDL.getInstance().init(application)
} catch (e: YoutubeDLException) {
@ -52,14 +69,15 @@ class ShareRecieverActivity : ComponentActivity() {
}
setContent {
CasterTheme {
val viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
if (!isViewModelInitialised())
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 {
@ -73,7 +91,16 @@ class ShareRecieverActivity : ComponentActivity() {
) {
item {
if (chromeCasts.isNotEmpty())
Text(text = "Available chromecasts:")
Text(text = stringResource(id = R.string.available_chromecasts))
else {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(id = R.string.looking_for_devices))
CircularProgressIndicator()
}
}
}
items(chromeCasts.size) { index ->
showChromeCastButton(
@ -83,8 +110,8 @@ class ShareRecieverActivity : ComponentActivity() {
link = link
)
}
item{
if(enabled.not()){
item {
if (enabled.not()) {
CircularProgressIndicator()
}
}
@ -103,21 +130,23 @@ class ShareRecieverActivity : ComponentActivity() {
chromeCast: ChromeCast,
link: String
) {
Button(
onClick = {
showChromeCastButton(chromeCast = chromeCast, buttonCallBack = {
if (enabled) {
onEnableChanged(false)
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)
}
lifecycleScope.launch {
ChromeCastHelper.chromeCast = chromeCast
ChromeCastHelper.castLink(link) {
finish()
//finish()
}
}
},
modifier = Modifier.padding(5.dp),
enabled = enabled
) {
Text(text = chromeCast.model)
}
})
}
@Composable
@ -131,16 +160,7 @@ class ShareRecieverActivity : ComponentActivity() {
IconButton(onClick = refresh) {
Icon(
Icons.Filled.Refresh,
contentDescription = "Refresh"
)
}
IconButton(onClick = {
}) {
Icon(
Icons.Filled.Add,
contentDescription = "Add"
contentDescription = stringResource(id = R.string.refresh)
)
}
}
@ -151,7 +171,7 @@ class ShareRecieverActivity : ComponentActivity() {
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
override fun DefaultPreview() {
CasterTheme {
MenuBar {}
}

View file

@ -0,0 +1,129 @@
package eu.toldi.balazs.caster.helpers
import android.annotation.SuppressLint
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import android.webkit.MimeTypeMap
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.Executors
class FileCacheHelper(
private val mContext: Context
) {
// content resolver
private val contentResolver = mContext.contentResolver
// to get the type of file
private val mimeTypeMap = MimeTypeMap.getSingleton()
private val mCacheLocation = mContext.cacheDir
fun cacheThis(uris: List<Uri>) {
executor.submit {
uris.forEach { uri -> copyFromSource(uri) }
}
}
fun tryFileName(uri: Uri): String{
val fileExtension: String = getFileExtension(uri) ?: kotlin.run {
throw RuntimeException("Extension is null for $uri")
}
return queryName(uri) ?: getFileName(fileExtension)
}
/**
* Copies the actual data from provided content provider.
*/
private fun copyFromSource(uri: Uri) {
val fileExtension: String = getFileExtension(uri) ?: kotlin.run {
throw RuntimeException("Extension is null for $uri")
}
val fileName = queryName(uri) ?: getFileName(fileExtension)
val inputStream = contentResolver.openInputStream(uri) ?: kotlin.run {
throw RuntimeException("Cannot open for reading $uri")
}
val bufferedInputStream = BufferedInputStream(inputStream)
// the file which will be the new cached file
val outputFile = File(mCacheLocation, fileName)
val bufferedOutputStream = BufferedOutputStream(FileOutputStream(outputFile))
// this will hold the content for each iteration
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var readBytes = 0 // will be -1 if reached the end of file
while (true) {
readBytes = bufferedInputStream.read(buffer)
// check if the read was failure
if (readBytes == -1) {
bufferedOutputStream.flush()
break
}
bufferedOutputStream.write(buffer)
bufferedOutputStream.flush()
}
Log.i("FileCache",outputFile.absoluteFile.toString())
// close everything
inputStream.close()
bufferedInputStream.close()
bufferedOutputStream.close()
}
private fun getFileExtension(uri: Uri): String? {
return mimeTypeMap.getExtensionFromMimeType(contentResolver.getType(uri))
}
/**
* Tries to get actual name of the file being copied.
* This might be required in some of the cases where you might want to know the file name too.
*
* @param uri
*
*/
@SuppressLint("Recycle")
private fun queryName(uri: Uri): String? {
val returnCursor: Cursor = contentResolver.query(uri, null, null, null, null) ?: return null
val nameIndex: Int = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
returnCursor.moveToFirst()
val name: String = returnCursor.getString(nameIndex)
returnCursor.close()
return name
}
private fun getFileName(fileExtension: String): String {
return "${System.currentTimeMillis().toString()}.$fileExtension"
}
/**
* Remove everything that we have cached.
* You might want to invoke this method before quiting the application.
*/
fun removeAll() {
mContext.cacheDir.deleteRecursively()
}
companion object {
// base buffer size
private const val BASE_BUFFER_SIZE = 1024
// if you want to modify size use binary multiplier 2, 4, 6, 8
private const val DEFAULT_BUFFER_SIZE = BASE_BUFFER_SIZE * 4
private val executor = Executors.newSingleThreadExecutor()
}
}

View file

@ -1,41 +1,46 @@
package eu.toldi.balazs.caster.model
import android.net.wifi.WifiManager
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.*
import su.litvak.chromecast.api.v2.ChromeCast
import su.litvak.chromecast.api.v2.ChromeCasts
import su.litvak.chromecast.api.v2.ChromeCastsListener
import java.lang.String
import java.net.InetAddress
import java.net.NetworkInterface
import javax.jmdns.ServiceEvent
import javax.jmdns.ServiceListener
import java.net.UnknownHostException
class ChromeCastViewModel : ViewModel(),ChromeCastsListener {
private val _chromecasts : MutableLiveData<List<ChromeCast>> = MutableLiveData<List<ChromeCast>>(listOf())
val chromeCasts : LiveData<List<ChromeCast>>
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}")
if (chromeCast != null) {
Log.i(null, "Found ${chromeCast.title}")
_chromecasts.postValue(_chromecasts.value!!.plus(chromeCast))
}
}
override fun chromeCastRemoved(chromeCast: ChromeCast?) {
if(chromeCast != null) {
if (chromeCast != null) {
Log.i(null,"Lost ${chromeCast.title}")
_chromecasts.postValue(_chromecasts.value!!.minus(chromeCast))
}
@ -50,7 +55,13 @@ class ChromeCastViewModel : ViewModel(),ChromeCastsListener {
fun startScanning() {
viewModelScope.launch(IO) {
ChromeCasts.startDiscovery(getIPv4Address())
val address = getIPv4Address()
if (address.toString().startsWith('/')) {
Log.e("Caster", address.toString().drop(1))
ChromeCasts.startDiscovery(InetAddress.getByName(address.toString().drop(1)))
} else {
ChromeCasts.startDiscovery(address)
}
}
}
fun getIPv4Address(): InetAddress? {
@ -64,4 +75,24 @@ class ChromeCastViewModel : ViewModel(),ChromeCastsListener {
return null
}
private fun getDeviceIpAddress(wifi: WifiManager): InetAddress? {
var result: InetAddress? = null
try {
// default to Android localhost
result = InetAddress.getByName("10.0.0.2")
// figure out our wifi address, otherwise bail
val wifiinfo = wifi.connectionInfo
val intaddr = wifiinfo.ipAddress
val byteaddr = byteArrayOf(
(intaddr and 0xff).toByte(), (intaddr shr 8 and 0xff).toByte(),
(intaddr shr 16 and 0xff).toByte(), (intaddr shr 24 and 0xff).toByte()
)
result = InetAddress.getByAddress(byteaddr)
} catch (ex: UnknownHostException) {
Log.w("Caster", String.format("getDeviceIpAddress Error: %s", ex.message))
}
return result
}
}

View file

@ -1,19 +1,21 @@
package eu.toldi.balazs.caster.model
import android.net.Uri
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 eu.toldi.balazs.caster.services.ChromecastManagerService
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
import java.net.InetAddress
import java.net.NetworkInterface
class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListener {
class ChromecastManageViewmodel : ViewModel() {
private val _mediaState = mutableStateOf<MediaStatus?>(null)
val mediaStatus: State<MediaStatus?>
@ -23,13 +25,14 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
init {
viewModelScope.launch(IO) {
fetchMediaStatus()
while (true) {
fetchMediaStatus()
Thread.sleep(1000)
Thread.sleep(3000)
}
}
}
/*
override fun spontaneousEventReceived(event: ChromeCastSpontaneousEvent?) {
Log.e(null, event?.type.toString())
if (event != null) {
@ -37,11 +40,15 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
_mediaState.value = event.data as MediaStatus
}
}
}
}*/
fun fetchMediaStatus() {
viewModelScope.launch(IO) {
try {
_mediaState.value = ChromeCastHelper.fetchMediaStatus()
} catch (e: Exception) {
Log.e(null, e.stackTraceToString())
}
}
}
@ -57,6 +64,13 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
}
}
fun castFromCache(fileName: String,callBack: () -> Unit = {}){
//Log.i("Caster","http://"+getIPv4Address()?.hostAddress+":"+ChromecastManagerService.PORT+"/assets/"+fileName)
viewModelScope.launch(IO) {
ChromeCastHelper.castLink("http://"+getIPv4Address()?.hostAddress+":"+ChromecastManagerService.PORT+"/assets/"+fileName,callBack)
}
}
fun stopApp() {
viewModelScope.launch(IO) {
ChromeCastHelper.stopApp()
@ -80,4 +94,27 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
ChromeCastHelper.setVolume(f)
}
}
fun increaseVolume(){
viewModelScope.launch(IO) {
setVolume(chromeCast.status.volume.level + 0.05f)
}
}
fun decreaseVolume(){
viewModelScope.launch(IO) {
setVolume(chromeCast.status.volume.level - 0.05f)
}
}
fun getIPv4Address(): InetAddress? {
NetworkInterface.getNetworkInterfaces().toList().forEach { interf ->
interf.inetAddresses.toList().forEach { inetAddress ->
if (!inetAddress.isLoopbackAddress && inetAddress.hostAddress.indexOf(':') < 0) {
return inetAddress
}
}
}
return null
}
}

View file

@ -6,9 +6,6 @@ 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
@ -19,13 +16,21 @@ 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 io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.jetty.*
import io.ktor.websocket.*
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
import java.io.File
class ChromecastManagerService : Service() {
@ -38,6 +43,9 @@ class ChromecastManagerService : Service() {
const val ACTION_NEXT = "action_next"
const val ACTION_PREVIOUS = "action_previous"
const val ACTION_STOP = "action_stop"
const val ACTION_SETFILE = "action_file"
const val PORT = 3080
}
private val mMediaPlayer = MediaPlayer()
@ -45,10 +53,33 @@ class ChromecastManagerService : Service() {
private lateinit var mController: MediaControllerCompat
private lateinit var pendingIntent : PendingIntent
private var mediaStatus: MediaStatus? = null
private var file : File? = null
override fun onBind(p0: Intent?): IBinder? = null
private lateinit var chromeCast: ChromeCast
private val server by lazy {
embeddedServer(Jetty, PORT, watchPaths = emptyList()) {
install(WebSockets)
install(CallLogging)
routing {
get("/") {
if(file == null) {
call.respondText(
text = "Hello!! You are here in ${Build.MODEL}",
contentType = ContentType.Text.Plain
)
}else{
call.respondFile(file!!)
}
}
static("assets") {
staticRootFolder = applicationContext.cacheDir
files(".")
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -74,7 +105,8 @@ class ChromecastManagerService : Service() {
chromeCast = ChromeCast(address)
chromeCast.name = name
val notificationIntent = Intent(this, ChromecastManagerService::class.java)
pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)
pendingIntent =
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_MUTABLE)
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Caster for ${chromeCast.name}@${address}")
@ -82,8 +114,11 @@ class ChromecastManagerService : Service() {
.setContentIntent(pendingIntent).build()
startForeground(1, notification)
CoroutineScope(IO).launch {
server.start(wait = true)
}
CoroutineScope(IO).launch {
GlobalScope.launch(IO) {
while (true) {
try {
ChromeCastHelper.chromeCast = chromeCast
@ -100,7 +135,7 @@ class ChromecastManagerService : Service() {
} catch (e: Exception) {
Log.e(null,e.stackTraceToString())
}
Thread.sleep(1000)
Thread.sleep(10500)
}
}
}
@ -117,6 +152,11 @@ class ChromecastManagerService : Service() {
}
override fun onDestroy() {
server.stop(1_000, 2_000)
super.onDestroy()
}
private suspend fun buildNotification() {
var status = chromeCast.status
Log.d(null, status.applications.toString())

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -2,4 +2,21 @@
<string name="app_name">Caster</string>
<string name="title_activity_chromecast_manager">ChromecastManagerActivity</string>
<string name="title_activity_share_reciever">Choose Chromecast</string>
<string name="refresh">Refresh</string>
<string name="add_chromecast">Add Chromecast</string>
<string name="cancel">Cancel</string>
<string name="ip_address">IP address of the Chromecast</string>
<string name="available_chromecasts">Available Chromecasts:</string>
<string name="looking_for_devices">Looking for Chromecasts on your network...</string>
<string name="cast">Cast</string>
<string name="stop">Stop</string>
<string name="pause">Pause</string>
<string name="resume">Resume</string>
<string name="play">Play</string>
<string name="fastforward">Fast Forward</string>
<string name="rewind">Rewind</string>
<string name="back">Back</string>
<string name="now_playing">Now playing</string>
<string name="cast_url">Cast URL</string>
</resources>

View file

@ -8,7 +8,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.2"
classpath 'com.android.tools.build:gradle:7.0.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
// NOTE: Do not place your application dependencies here; they belong