Compare commits
10 commits
cf4374d34b
...
8865f2c5b4
Author | SHA1 | Date | |
---|---|---|---|
8865f2c5b4 | |||
5933cd2b8a | |||
55ca983cf7 | |||
f681b7afc9 | |||
a55f2bcd44 | |||
5672eab2bd | |||
0943680271 | |||
4c88992e5f | |||
bebde9bc6a | |||
0432c0be90 |
37
.idea/deploymentTargetDropDown.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
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.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
// 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(),
|
||||
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"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,7 +69,8 @@ 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
|
||||
|
@ -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(
|
||||
|
@ -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 {}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
}
|
||||
}
|
|
@ -1,32 +1,37 @@
|
|||
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())
|
||||
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}")
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
|
|
BIN
app/src/main/res/drawable-hdpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
app/src/main/res/drawable-hdpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
app/src/main/res/drawable-ldpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/drawable-ldpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/drawable-mdpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/drawable-mdpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/drawable-xhdpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
app/src/main/res/drawable-xhdpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
app/src/main/res/drawable-xxhdpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
app/src/main/res/drawable-xxhdpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 21 KiB |
|
@ -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>
|
|
@ -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
|
||||
|
|