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-1637656156044.xml" value="0.25" />
<entry key="../../../../../layout/compose-model-1637673325505.xml" value="0.11570945945945946" /> <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-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> </map>
</option> </option>
</component> </component>

View file

@ -4,6 +4,8 @@ plugins {
} }
android { android {
compileSdk 31 compileSdk 31
defaultConfig { defaultConfig {
@ -17,6 +19,11 @@ android {
vectorDrawables { vectorDrawables {
useSupportLibrary true useSupportLibrary true
} }
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
} }
buildTypes { buildTypes {
@ -29,6 +36,16 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility 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 { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '1.8'
useIR = true useIR = true
@ -41,6 +58,10 @@ android {
kotlinCompilerVersion '1.5.21' kotlinCompilerVersion '1.5.21'
} }
packagingOptions { packagingOptions {
exclude 'META-INF/*'
// Due to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
exclude 'META-INF/licenses/*'
exclude '**/attach_hotspot_windows.dll'
resources { resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}' excludes += '/META-INF/{AL2.0,LGPL2.1}'
} }
@ -62,9 +83,15 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
implementation 'su.litvak.chromecast:api-v2:0.11.3' implementation 'su.litvak.chromecast:api-v2:0.11.3'
implementation 'com.github.yausername.youtubedl-android:library:0.12.+' implementation("io.coil-kt:coil-compose:1.4.0")
implementation 'com.github.yausername.youtubedl-android:ffmpeg:0.12.+' 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 "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.+' testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View file

@ -3,8 +3,9 @@
package="eu.toldi.balazs.caster"> package="eu.toldi.balazs.caster">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application <application
android:name=".App" android:name=".App"
@ -13,6 +14,8 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@drawable/ic_caster_logo" android:roundIcon="@drawable/ic_caster_logo"
android:supportsRtl="true" android:supportsRtl="true"
android:extractNativeLibs="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/Theme.Caster"> android:theme="@style/Theme.Caster">
<activity <activity
android:name=".ShareRecieverActivity" android:name=".ShareRecieverActivity"
@ -24,6 +27,16 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent-filter> </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>
<activity <activity
android:name=".ChromecastManagerActivity" 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.Application
import su.litvak.chromecast.api.v2.ChromeCast import su.litvak.chromecast.api.v2.ChromeCast
import su.litvak.chromecast.api.v2.MediaStatus import su.litvak.chromecast.api.v2.MediaStatus
import java.net.InetAddress
import java.net.NetworkInterface
object ChromeCastHelper { object ChromeCastHelper {
@ -25,6 +27,7 @@ object ChromeCastHelper {
const val APP_BUBBLEUPNP = "3927FA74" const val APP_BUBBLEUPNP = "3927FA74"
const val APP_BBCSOUNDS = "03977A48" const val APP_BBCSOUNDS = "03977A48"
const val APP_BBCIPLAYER = "5E81F6DB" const val APP_BBCIPLAYER = "5E81F6DB"
const val APP_YOUTUBE_MUSIC = "2DB7CC49"
lateinit var chromeCast: ChromeCast lateinit var chromeCast: ChromeCast
@ -32,6 +35,7 @@ object ChromeCastHelper {
return when (id) { return when (id) {
APP_MEDIA_RECEIVER -> "Media Player" APP_MEDIA_RECEIVER -> "Media Player"
APP_YOUTUBE -> "Youtube" APP_YOUTUBE -> "Youtube"
APP_YOUTUBE_MUSIC -> "Youtube Music"
APP_PLEX -> "Plex" APP_PLEX -> "Plex"
APP_BACKDROP -> "Idle" APP_BACKDROP -> "Idle"
APP_DASHCAST -> "Dashcast" APP_DASHCAST -> "Dashcast"
@ -66,7 +70,11 @@ object ChromeCastHelper {
if (mediaStatus != null) { if (mediaStatus != null) {
try { try {
withContext(IO) { withContext(IO) {
chromeCast.seek(d * mediaStatus.media.duration) try {
chromeCast.seek(d * mediaStatus.media.duration)
}catch (e:Exception) {
Log.e(null,e.stackTraceToString())
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(null, e.stackTraceToString()) Log.e(null, e.stackTraceToString())
@ -96,7 +104,13 @@ object ChromeCastHelper {
suspend fun setVolume(f: Float) { suspend fun setVolume(f: Float) {
withContext(IO) { withContext(IO) {
chromeCast.setVolume(f) if(f >= 0.0f && f <= 1.0f) {
try {
chromeCast.setVolume(f)
}catch (e:Exception) {
Log.e(null,e.stackTraceToString())
}
}
} }
} }
@ -120,30 +134,43 @@ object ChromeCastHelper {
exitStatus = false exitStatus = false
callBack.invoke() callBack.invoke()
} else { } else {
try {
val streamInfo = _streamInfo as VideoInfo
val status = chromeCast.status
if (chromeCast.isAppAvailable(ChromeCastHelper.APP_MEDIA_RECEIVER) && !status.isAppRunning(
ChromeCastHelper.APP_MEDIA_RECEIVER
)
) {
val app: Application =
chromeCast.launchApp(ChromeCastHelper.APP_MEDIA_RECEIVER)
}
while (!chromeCast.status.isAppRunning(ChromeCastHelper.APP_MEDIA_RECEIVER)) {
delay(100)
}
val streamInfo = _streamInfo as VideoInfo chromeCast.load(
val status = chromeCast.status streamInfo.title,
if (chromeCast.isAppAvailable(ChromeCastHelper.APP_MEDIA_RECEIVER) && !status.isAppRunning( streamInfo.thumbnail,
ChromeCastHelper.APP_MEDIA_RECEIVER streamInfo.url,
null
) )
) { } catch (e: Exception) {
val app: Application = chromeCast.launchApp(ChromeCastHelper.APP_MEDIA_RECEIVER) Log.e(null, e.stackTraceToString())
} finally {
callBack.invoke()
} }
while (!chromeCast.status.isAppRunning(ChromeCastHelper.APP_MEDIA_RECEIVER)) {
delay(100)
}
chromeCast.load(
streamInfo.title,
streamInfo.thumbnail,
streamInfo.url,
null
)
callBack.invoke()
} }
} }
return exitStatus 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 package eu.toldi.balazs.caster
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.OpenableColumns
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.webkit.MimeTypeMap
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -15,34 +23,31 @@ import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import coil.compose.rememberImagePainter
import eu.toldi.balazs.caster.ui.theme.CasterTheme import com.yausername.ffmpeg.FFmpeg
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 com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import com.yausername.youtubedl_android.YoutubeDLException import com.yausername.youtubedl_android.YoutubeDLException
import com.yausername.youtubedl_android.mapper.VideoInfo import eu.toldi.balazs.caster.helpers.FileCacheHelper
import eu.toldi.balazs.caster.model.ChromecastManageViewmodel import eu.toldi.balazs.caster.model.ChromecastManageViewmodel
import eu.toldi.balazs.caster.services.ChromecastManagerService 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.Media
import su.litvak.chromecast.api.v2.MediaStatus import su.litvak.chromecast.api.v2.MediaStatus
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.FileOutputStream
class ChromecastManagerActivity : ComponentActivity() { class ChromecastManagerActivity : ComponentActivity() {
companion object { companion object {
var chromeCast_: ChromeCast? = null var chromeCast_: ChromeCast? = null
const val METADATA_THUMB = "thumb"
} }
private lateinit var chromeCast: ChromeCast private lateinit var chromeCast: ChromeCast
@ -56,10 +61,11 @@ class ChromecastManagerActivity : ComponentActivity() {
ChromeCastHelper.chromeCast = chromeCast ChromeCastHelper.chromeCast = chromeCast
try { try {
YoutubeDL.getInstance().init(application) YoutubeDL.getInstance().init(application)
FFmpeg.getInstance().init(application)
} catch (e: YoutubeDLException) { } catch (e: YoutubeDLException) {
Log.e("Caster", "failed to initialize youtubedl-android", e) Log.e("Caster", "failed to initialize youtubedl-android", e)
} }
val serviceIntent = Intent(this, ChromecastManagerService::class.java).also { Intent(this, ChromecastManagerService::class.java).also {
it.action = ChromecastManagerService.ACTION_INIT it.action = ChromecastManagerService.ACTION_INIT
it.putExtra("CHROMECAST_ADDRESS", chromeCast.address) it.putExtra("CHROMECAST_ADDRESS", chromeCast.address)
it.putExtra("CHROMECAST_NAME", chromeCast.title) it.putExtra("CHROMECAST_NAME", chromeCast.title)
@ -72,7 +78,7 @@ class ChromecastManagerActivity : ComponentActivity() {
// A surface container using the 'background' color from the theme // A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) { Surface(color = MaterialTheme.colors.background) {
Column { Column {
viewModel.fetchMediaStatus() //viewModel.fetchMediaStatus()
MenuBar() MenuBar()
Column( Column(
modifier = Modifier modifier = Modifier
@ -88,7 +94,7 @@ class ChromecastManagerActivity : ComponentActivity() {
onValueChange = { onValueChange = {
text = it text = it
}, },
label = { Text("Cast URL") }, label = { Text(stringResource(id = R.string.cast_url)) },
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
) )
var castEnabled by remember { mutableStateOf(true) } var castEnabled by remember { mutableStateOf(true) }
@ -102,44 +108,13 @@ class ChromecastManagerActivity : ComponentActivity() {
}, },
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
) { ) {
Text(text = "Cast") Text(text = stringResource(id = R.string.cast))
} }
if (castEnabled.not()) { if (castEnabled.not()) {
CircularProgressIndicator() CircularProgressIndicator()
} }
playBackControl() PlayBackControl()
val mediaStatus = viewModel.mediaStatus.value mediaStatus()
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()) }
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = String.format(
"%02d:%02d",
((mediaStatus.currentTime % 3600) / 60).toInt(),
(mediaStatus.currentTime % 60).toInt()
)
)
Text(
text = String.format(
"%02d:%02d",
((mediaStatus.media.duration % 3600) / 60).toInt(),
(mediaStatus.media.duration % 60).toInt()
)
)
}
Slider(value = sliderPosition, onValueChange = {
sliderPosition = it
}, onValueChangeFinished = {
viewModel.seek(sliderPosition.toDouble())
})
}
} }
} }
} }
@ -148,8 +123,90 @@ class ChromecastManagerActivity : ComponentActivity() {
} }
@Composable
fun mediaStatus() {
val mediaStatus = viewModel.mediaStatus.value
if (mediaStatus != null) {
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
) {
Text(
text = String.format(
"%02d:%02d",
((mediaStatus.currentTime % 3600) / 60).toInt(),
(mediaStatus.currentTime % 60).toInt()
)
)
Text(
text = String.format(
"%02d:%02d",
((mediaStatus.media.duration % 3600) / 60).toInt(),
(mediaStatus.media.duration % 60).toInt()
)
)
}
Slider(value = sliderPosition, onValueChange = {
sliderPosition = it
sliderMoving = true
}, onValueChangeFinished = {
viewModel.seek(sliderPosition.toDouble())
sliderMoving = false
})
}
}
}
@Composable @Composable
fun MenuBar() { 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( TopAppBar(
title = { title = {
Text("Caster") Text("Caster")
@ -158,39 +215,42 @@ class ChromecastManagerActivity : ComponentActivity() {
IconButton(onClick = { finish() }) { IconButton(onClick = { finish() }) {
Icon( Icon(
Icons.Filled.ArrowBack, Icons.Filled.ArrowBack,
contentDescription = "Back" contentDescription = stringResource(id = R.string.back)
) )
} }
}, },
actions = { actions = {
Row { Row {
IconButton(onClick = {
pickPictureLauncher.launch(arrayOf("image/*", "video/*"))
}) {
Icon(
Icons.Filled.Folder,
contentDescription = stringResource(id = R.string.back)
)
}
} }
} }
) )
} }
@Composable @Composable
fun playBackControl() { fun PlayBackControl() {
val mediaStatus = viewModel.mediaStatus.value val mediaStatus = if (this::viewModel.isInitialized)
viewModel.mediaStatus.value
else null
Row { Row {
IconButton(onClick = { IconButton(onClick = {
if (mediaStatus != null) { if (mediaStatus != null) {
viewModel.seek(chromeCast.mediaStatus.currentTime - 10) viewModel.seek(mediaStatus.currentTime - 10)
} }
}, enabled = mediaStatus != null) { }, enabled = mediaStatus != null) {
Icon( Icon(
Icons.Filled.FastRewind, 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 = { IconButton(onClick = {
if (mediaStatus != null) { if (mediaStatus != null) {
@ -200,22 +260,21 @@ class ChromecastManagerActivity : ComponentActivity() {
if (mediaStatus.playerState == MediaStatus.PlayerState.PAUSED) { if (mediaStatus.playerState == MediaStatus.PlayerState.PAUSED) {
viewModel.play() viewModel.play()
} }
viewModel.fetchMediaStatus()
} }
}, enabled = mediaStatus != null) { }, enabled = mediaStatus != null) {
when { when {
mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.PAUSED -> Icon( mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.PAUSED -> Icon(
Icons.Filled.PlayArrow, Icons.Filled.PlayArrow,
contentDescription = "Resume" contentDescription = stringResource(id = R.string.resume)
) )
mediaStatus.playerState == MediaStatus.PlayerState.PLAYING -> Icon( mediaStatus.playerState == MediaStatus.PlayerState.PLAYING -> Icon(
Icons.Filled.Pause, Icons.Filled.Pause,
contentDescription = "Pause" contentDescription = stringResource(id = R.string.pause)
) )
else -> Icon( else -> Icon(
Icons.Filled.PlayArrow, Icons.Filled.PlayArrow,
contentDescription = "Resume" contentDescription = stringResource(id = R.string.resume)
) )
} }
@ -226,18 +285,18 @@ class ChromecastManagerActivity : ComponentActivity() {
}, enabled = mediaStatus != null) { }, enabled = mediaStatus != null) {
Icon( Icon(
Icons.Filled.Stop, Icons.Filled.Stop,
contentDescription = "Stop" contentDescription = stringResource(id = R.string.stop)
) )
} }
IconButton(onClick = { IconButton(onClick = {
if (chromeCast.mediaStatus != null) { if (mediaStatus != null) {
viewModel.seek(chromeCast.mediaStatus.currentTime + 10) viewModel.seek(mediaStatus.currentTime + 10)
} }
}, enabled = mediaStatus != null) { }, enabled = mediaStatus != null) {
Icon( Icon(
Icons.Filled.FastForward, Icons.Filled.FastForward,
contentDescription = "FastForward" contentDescription = stringResource(id = R.string.fastforward)
) )
} }
} }
@ -270,18 +329,18 @@ class ChromecastManagerActivity : ComponentActivity() {
onValueChange = { onValueChange = {
text = it text = it
}, },
label = { Text("Cast URL") }, label = { Text(stringResource(id = R.string.cast_url)) },
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
) )
Button( Button(
onClick = {}, onClick = {},
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
) { ) {
Text(text = "Cast") Text(text = stringResource(id = R.string.cast))
} }
// val viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java) // val viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java)
//playBackControl(viewModel) PlayBackControl()
Text(text = "Now playing: Some video") Text(text = stringResource(id = R.string.now_playing) + ": Some video")
var sliderPosition by remember { mutableStateOf(0f) } var sliderPosition by remember { mutableStateOf(0f) }
Slider(value = sliderPosition, onValueChange = { sliderPosition = it }) Slider(value = sliderPosition, onValueChange = { sliderPosition = it })
} }
@ -291,13 +350,13 @@ class ChromecastManagerActivity : ComponentActivity() {
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
viewModel.setVolume(chromeCast.status.volume.level + 0.05f) viewModel.increaseVolume()
return true return true
} }
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
viewModel.decreaseVolume()
viewModel.setVolume(chromeCast.status.volume.level - 0.05f)
return true return true
} }
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)

View file

@ -2,36 +2,64 @@ package eu.toldi.balazs.caster
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.MulticastLock
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent 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.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons 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.*
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider 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.model.ChromeCastViewModel
import eu.toldi.balazs.caster.ui.theme.CasterTheme 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.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") @SuppressLint("CoroutineCreationDuringComposition")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -39,37 +67,76 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
CasterTheme { 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 chromeCastState = viewModel.chromeCasts.observeAsState(initial = emptyList())
val chromeCasts = chromeCastState.value val chromeCasts = chromeCastState.value
Log.e(null,chromeCasts.toString()) Log.e(null, chromeCasts.toString())
// A surface container using the 'background' color from the theme // A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) { Surface(color = MaterialTheme.colors.background) {
Column { Column(modifier = Modifier
MenuBar { .fillMaxWidth()
viewModel.refresh() .fillMaxHeight()) {
var isAddChromecastDialogOpen by remember {
mutableStateOf(false)
} }
LazyColumn(modifier = Modifier MenuBar(
.padding(all = 4.dp) refresh = {
.fillMaxWidth(), viewModel.refresh()
horizontalAlignment = Alignment.CenterHorizontally) { },
item { add = {
if (chromeCasts.isNotEmpty()) isAddChromecastDialogOpen = true
Text(text = "Available chromecasts:") })
else {
Column(
modifier = Modifier.fillMaxSize() , if (isAddChromecastDialogOpen) {
horizontalAlignment = Alignment.CenterHorizontally showAddChromecastDialog(dismiss = {
){ isAddChromecastDialogOpen = false
Text("Looking for Chromecasts on your network...") }) {
CircularProgressIndicator() 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()
.fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
if (chromeCasts.isNotEmpty())
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 ->
items(chromeCasts.size) { index -> showChromeCastButton(chromeCast = chromeCasts[index])
showChromeCastButton(chromeCast = chromeCasts[index]) }
} }
} }
} }
@ -78,22 +145,106 @@ class MainActivity : ComponentActivity() {
} }
} }
@Composable
fun showAddChromecastDialog(dismiss: () -> Unit, add: (String) -> Unit) {
var ipaddress by remember {
mutableStateOf("")
}
AlertDialog(onDismissRequest = dismiss,
title = {
Text(text = stringResource(id = R.string.add_chromecast))
},
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
) {
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 @Composable
fun showChromeCastButton(chromeCast: ChromeCast) { fun showChromeCastButton(chromeCast: ChromeCast,buttonCallBack: () -> Unit = {
Button(onClick = { ChromecastManagerActivity.chromeCast_ = chromeCast
ChromecastManagerActivity.chromeCast_ = chromeCast val intent = Intent(applicationContext, ChromecastManagerActivity::class.java)
val intent = Intent(this, ChromecastManagerActivity::class.java) startActivity(intent)
startActivity(intent) }) {
}, Column(
modifier = Modifier.padding(5.dp) Modifier
.fillMaxWidth()
.padding(all = 5.dp)
) { ) {
Text(text = chromeCast.model) 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 @Composable
fun MenuBar(refresh: () -> Unit) { fun MenuBar(refresh: () -> Unit, add: () -> Unit) {
TopAppBar( TopAppBar(
title = { title = {
Text("Caster") Text("Caster")
@ -103,16 +254,14 @@ class MainActivity : ComponentActivity() {
IconButton(onClick = refresh) { IconButton(onClick = refresh) {
Icon( Icon(
Icons.Filled.Refresh, Icons.Filled.Refresh,
contentDescription = "Refresh" contentDescription = stringResource(id = R.string.refresh)
) )
} }
IconButton(onClick = { IconButton(onClick = add) {
}) {
Icon( Icon(
Icons.Filled.Add, Icons.Filled.Add,
contentDescription = "Add" contentDescription = stringResource(id = R.string.add_chromecast)
) )
} }
} }
@ -123,9 +272,26 @@ class MainActivity : ComponentActivity() {
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun DefaultPreview() { open fun DefaultPreview() {
CasterTheme { 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.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.util.Log import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons 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.*
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -21,30 +21,47 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLException 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.model.ChromeCastViewModel
import eu.toldi.balazs.caster.services.ChromecastManagerService
import eu.toldi.balazs.caster.ui.theme.CasterTheme import eu.toldi.balazs.caster.ui.theme.CasterTheme
import kotlinx.coroutines.* import kotlinx.coroutines.launch
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.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") @SuppressLint("CoroutineCreationDuringComposition")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (intent?.action != Intent.ACTION_SEND || intent.type != "text/plain") { if (intent?.action != Intent.ACTION_SEND ) {
finish() 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 { try {
YoutubeDL.getInstance().init(application) YoutubeDL.getInstance().init(application)
} catch (e: YoutubeDLException) { } catch (e: YoutubeDLException) {
@ -52,14 +69,15 @@ class ShareRecieverActivity : ComponentActivity() {
} }
setContent { setContent {
CasterTheme { CasterTheme {
val viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java) if (!isViewModelInitialised())
viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
viewModel.startScanning() viewModel.startScanning()
val chromeCastState = viewModel.chromeCasts.observeAsState(emptyList()) val chromeCastState = viewModel.chromeCasts.observeAsState(emptyList())
val chromeCasts = chromeCastState.value val chromeCasts = chromeCastState.value
// A surface container using the 'background' color from the theme // A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) { Surface(color = MaterialTheme.colors.background) {
Column { Column {
MenuBar{ MenuBar {
viewModel.refresh() viewModel.refresh()
} }
var enabled by remember { var enabled by remember {
@ -73,7 +91,16 @@ class ShareRecieverActivity : ComponentActivity() {
) { ) {
item { item {
if (chromeCasts.isNotEmpty()) 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 -> items(chromeCasts.size) { index ->
showChromeCastButton( showChromeCastButton(
@ -83,8 +110,8 @@ class ShareRecieverActivity : ComponentActivity() {
link = link link = link
) )
} }
item{ item {
if(enabled.not()){ if (enabled.not()) {
CircularProgressIndicator() CircularProgressIndicator()
} }
} }
@ -103,21 +130,23 @@ class ShareRecieverActivity : ComponentActivity() {
chromeCast: ChromeCast, chromeCast: ChromeCast,
link: String link: String
) { ) {
Button( showChromeCastButton(chromeCast = chromeCast, buttonCallBack = {
onClick = { if (enabled) {
onEnableChanged(false) 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 { lifecycleScope.launch {
ChromeCastHelper.chromeCast = chromeCast ChromeCastHelper.chromeCast = chromeCast
ChromeCastHelper.castLink(link) { ChromeCastHelper.castLink(link) {
finish() //finish()
} }
} }
}, }
modifier = Modifier.padding(5.dp), })
enabled = enabled
) {
Text(text = chromeCast.model)
}
} }
@Composable @Composable
@ -131,16 +160,7 @@ class ShareRecieverActivity : ComponentActivity() {
IconButton(onClick = refresh) { IconButton(onClick = refresh) {
Icon( Icon(
Icons.Filled.Refresh, Icons.Filled.Refresh,
contentDescription = "Refresh" contentDescription = stringResource(id = R.string.refresh)
)
}
IconButton(onClick = {
}) {
Icon(
Icons.Filled.Add,
contentDescription = "Add"
) )
} }
} }
@ -151,7 +171,7 @@ class ShareRecieverActivity : ComponentActivity() {
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun DefaultPreview() { override fun DefaultPreview() {
CasterTheme { CasterTheme {
MenuBar {} 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 package eu.toldi.balazs.caster.model
import android.net.wifi.WifiManager
import android.util.Log 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.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch 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.InetAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import javax.jmdns.ServiceEvent import java.net.UnknownHostException
import javax.jmdns.ServiceListener
class ChromeCastViewModel : ViewModel(),ChromeCastsListener {
private val _chromecasts : MutableLiveData<List<ChromeCast>> = MutableLiveData<List<ChromeCast>>(listOf()) class ChromeCastViewModel : ViewModel(), ChromeCastsListener {
val chromeCasts : LiveData<List<ChromeCast>> private val _chromecasts: MutableLiveData<List<ChromeCast>> =
MutableLiveData<List<ChromeCast>>(listOf())
val chromeCasts: LiveData<List<ChromeCast>>
get() = _chromecasts get() = _chromecasts
init { init {
ChromeCasts.registerListener(this) ChromeCasts.registerListener(this)
} }
fun addChromecast(chromeCast: ChromeCast) {
_chromecasts.postValue(_chromecasts.value!!.plus(chromeCast))
}
override fun newChromeCastDiscovered(chromeCast: ChromeCast?) { override fun newChromeCastDiscovered(chromeCast: ChromeCast?) {
if(chromeCast != null) { if (chromeCast != null) {
Log.i(null,"Found ${chromeCast.title}") Log.i(null, "Found ${chromeCast.title}")
_chromecasts.postValue(_chromecasts.value!!.plus(chromeCast)) _chromecasts.postValue(_chromecasts.value!!.plus(chromeCast))
} }
} }
override fun chromeCastRemoved(chromeCast: ChromeCast?) { override fun chromeCastRemoved(chromeCast: ChromeCast?) {
if(chromeCast != null) { if (chromeCast != null) {
Log.i(null,"Lost ${chromeCast.title}") Log.i(null,"Lost ${chromeCast.title}")
_chromecasts.postValue(_chromecasts.value!!.minus(chromeCast)) _chromecasts.postValue(_chromecasts.value!!.minus(chromeCast))
} }
@ -50,7 +55,13 @@ class ChromeCastViewModel : ViewModel(),ChromeCastsListener {
fun startScanning() { fun startScanning() {
viewModelScope.launch(IO) { 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? { fun getIPv4Address(): InetAddress? {
@ -64,4 +75,24 @@ class ChromeCastViewModel : ViewModel(),ChromeCastsListener {
return null 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 package eu.toldi.balazs.caster.model
import android.net.Uri
import android.util.Log import android.util.Log
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import eu.toldi.balazs.caster.ChromeCastHelper import eu.toldi.balazs.caster.ChromeCastHelper
import eu.toldi.balazs.caster.services.ChromecastManagerService
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import su.litvak.chromecast.api.v2.ChromeCast 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 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) private val _mediaState = mutableStateOf<MediaStatus?>(null)
val mediaStatus: State<MediaStatus?> val mediaStatus: State<MediaStatus?>
@ -23,13 +25,14 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
init { init {
viewModelScope.launch(IO) { viewModelScope.launch(IO) {
fetchMediaStatus()
while (true) { while (true) {
fetchMediaStatus() fetchMediaStatus()
Thread.sleep(1000) Thread.sleep(3000)
} }
} }
} }
/*
override fun spontaneousEventReceived(event: ChromeCastSpontaneousEvent?) { override fun spontaneousEventReceived(event: ChromeCastSpontaneousEvent?) {
Log.e(null, event?.type.toString()) Log.e(null, event?.type.toString())
if (event != null) { if (event != null) {
@ -37,11 +40,15 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
_mediaState.value = event.data as MediaStatus _mediaState.value = event.data as MediaStatus
} }
} }
} }*/
fun fetchMediaStatus() { fun fetchMediaStatus() {
viewModelScope.launch(IO) { viewModelScope.launch(IO) {
_mediaState.value = ChromeCastHelper.fetchMediaStatus() 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() { fun stopApp() {
viewModelScope.launch(IO) { viewModelScope.launch(IO) {
ChromeCastHelper.stopApp() ChromeCastHelper.stopApp()
@ -80,4 +94,27 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
ChromeCastHelper.setVolume(f) 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.app.Service
import android.content.Intent import android.content.Intent
import android.media.MediaPlayer 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.Build
import android.os.IBinder import android.os.IBinder
import android.support.v4.media.session.MediaControllerCompat 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.App.Companion.CHANNEL_ID
import eu.toldi.balazs.caster.ChromeCastHelper import eu.toldi.balazs.caster.ChromeCastHelper
import eu.toldi.balazs.caster.R 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.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import su.litvak.chromecast.api.v2.ChromeCast import su.litvak.chromecast.api.v2.ChromeCast
import su.litvak.chromecast.api.v2.Media import su.litvak.chromecast.api.v2.Media
import su.litvak.chromecast.api.v2.MediaStatus import su.litvak.chromecast.api.v2.MediaStatus
import java.io.File
class ChromecastManagerService : Service() { class ChromecastManagerService : Service() {
@ -38,6 +43,9 @@ class ChromecastManagerService : Service() {
const val ACTION_NEXT = "action_next" const val ACTION_NEXT = "action_next"
const val ACTION_PREVIOUS = "action_previous" const val ACTION_PREVIOUS = "action_previous"
const val ACTION_STOP = "action_stop" const val ACTION_STOP = "action_stop"
const val ACTION_SETFILE = "action_file"
const val PORT = 3080
} }
private val mMediaPlayer = MediaPlayer() private val mMediaPlayer = MediaPlayer()
@ -45,10 +53,33 @@ class ChromecastManagerService : Service() {
private lateinit var mController: MediaControllerCompat private lateinit var mController: MediaControllerCompat
private lateinit var pendingIntent : PendingIntent private lateinit var pendingIntent : PendingIntent
private var mediaStatus: MediaStatus? = null private var mediaStatus: MediaStatus? = null
private var file : File? = null
override fun onBind(p0: Intent?): IBinder? = null override fun onBind(p0: Intent?): IBinder? = null
private lateinit var chromeCast: ChromeCast 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) @RequiresApi(Build.VERSION_CODES.M)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -74,7 +105,8 @@ class ChromecastManagerService : Service() {
chromeCast = ChromeCast(address) chromeCast = ChromeCast(address)
chromeCast.name = name chromeCast.name = name
val notificationIntent = Intent(this, ChromecastManagerService::class.java) 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) val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Caster for ${chromeCast.name}@${address}") .setContentTitle("Caster for ${chromeCast.name}@${address}")
@ -82,8 +114,11 @@ class ChromecastManagerService : Service() {
.setContentIntent(pendingIntent).build() .setContentIntent(pendingIntent).build()
startForeground(1, notification) startForeground(1, notification)
CoroutineScope(IO).launch {
server.start(wait = true)
}
CoroutineScope(IO).launch {
GlobalScope.launch(IO) {
while (true) { while (true) {
try { try {
ChromeCastHelper.chromeCast = chromeCast ChromeCastHelper.chromeCast = chromeCast
@ -100,7 +135,7 @@ class ChromecastManagerService : Service() {
} catch (e: Exception) { } catch (e: Exception) {
Log.e(null,e.stackTraceToString()) 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() { private suspend fun buildNotification() {
var status = chromeCast.status var status = chromeCast.status
Log.d(null, status.applications.toString()) 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="app_name">Caster</string>
<string name="title_activity_chromecast_manager">ChromecastManagerActivity</string> <string name="title_activity_chromecast_manager">ChromecastManagerActivity</string>
<string name="title_activity_share_reciever">Choose Chromecast</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> </resources>

View file

@ -8,7 +8,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong