mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-09-26 03:09:04 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
75e97ed008
11 changed files with 421 additions and 178 deletions
|
@ -56,7 +56,7 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
|
|
|
@ -31,6 +31,12 @@ import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InterfaceAddress
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
import java.net.SocketException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
@ -332,3 +338,97 @@ fun ByteArray.fromGzip(): ByteArray {
|
||||||
}
|
}
|
||||||
return outputStream.toByteArray()
|
return outputStream.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findPreferredAddress(): InetAddress? {
|
||||||
|
val candidates = NetworkInterface.getNetworkInterfaces()
|
||||||
|
.toList()
|
||||||
|
.asSequence()
|
||||||
|
.filter(::isUsableInterface)
|
||||||
|
.flatMap { nif ->
|
||||||
|
nif.interfaceAddresses
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull { ia ->
|
||||||
|
ia.address.takeIf(::isUsableAddress)?.let { addr ->
|
||||||
|
nif to ia
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
.minWithOrNull(
|
||||||
|
compareBy<Pair<NetworkInterface, InterfaceAddress>>(
|
||||||
|
{ addressScore(it.second.address) },
|
||||||
|
{ interfaceScore(it.first) },
|
||||||
|
{ -it.second.networkPrefixLength.toInt() },
|
||||||
|
{ -it.first.mtu }
|
||||||
|
)
|
||||||
|
)?.second?.address
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUsableInterface(nif: NetworkInterface): Boolean {
|
||||||
|
val name = nif.name.lowercase()
|
||||||
|
return try {
|
||||||
|
// must be up, not loopback/virtual/PtP, have a MAC, not Docker/tun/etc.
|
||||||
|
nif.isUp
|
||||||
|
&& !nif.isLoopback
|
||||||
|
&& !nif.isPointToPoint
|
||||||
|
&& !nif.isVirtual
|
||||||
|
&& !name.startsWith("docker")
|
||||||
|
&& !name.startsWith("veth")
|
||||||
|
&& !name.startsWith("br-")
|
||||||
|
&& !name.startsWith("virbr")
|
||||||
|
&& !name.startsWith("vmnet")
|
||||||
|
&& !name.startsWith("tun")
|
||||||
|
&& !name.startsWith("tap")
|
||||||
|
} catch (e: SocketException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUsableAddress(addr: InetAddress): Boolean {
|
||||||
|
return when {
|
||||||
|
addr.isAnyLocalAddress -> false // 0.0.0.0 / ::
|
||||||
|
addr.isLoopbackAddress -> false
|
||||||
|
addr.isLinkLocalAddress -> false // 169.254.x.x or fe80::/10
|
||||||
|
addr.isMulticastAddress -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun interfaceScore(nif: NetworkInterface): Int {
|
||||||
|
val name = nif.name.lowercase()
|
||||||
|
return when {
|
||||||
|
name.matches(Regex("^(eth|enp|eno|ens|em)\\d+")) -> 0
|
||||||
|
name.startsWith("eth") || name.contains("ethernet") -> 0
|
||||||
|
name.matches(Regex("^(wlan|wlp)\\d+")) -> 1
|
||||||
|
name.contains("wi-fi") || name.contains("wifi") -> 1
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addressScore(addr: InetAddress): Int {
|
||||||
|
return when (addr) {
|
||||||
|
is Inet4Address -> {
|
||||||
|
val octets = addr.address.map { it.toInt() and 0xFF }
|
||||||
|
when {
|
||||||
|
octets[0] == 10 -> 0 // 10/8
|
||||||
|
octets[0] == 192 && octets[1] == 168 -> 0 // 192.168/16
|
||||||
|
octets[0] == 172 && octets[1] in 16..31 -> 0 // 172.16–31/12
|
||||||
|
else -> 1 // public IPv4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Inet6Address -> {
|
||||||
|
// ULA (fc00::/7) vs global vs others
|
||||||
|
val b0 = addr.address[0].toInt() and 0xFF
|
||||||
|
when {
|
||||||
|
(b0 and 0xFE) == 0xFC -> 2 // ULA
|
||||||
|
(b0 and 0xE0) == 0x20 -> 3 // global
|
||||||
|
else -> 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Enumeration<T>.toList(): List<T> = Collections.list(this)
|
|
@ -33,6 +33,8 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.whenStateAtLeast
|
||||||
|
import androidx.lifecycle.withStateAtLeast
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.BuildConfig
|
import com.futo.platformplayer.BuildConfig
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
@ -203,7 +205,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
runBlocking {
|
lifecycleScope.launch {
|
||||||
handleUrlAll(content)
|
handleUrlAll(content)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
@ -280,7 +282,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
try {
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Unhandled exception in updateAvailableClients", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Preload common files to memory
|
//Preload common files to memory
|
||||||
|
@ -707,7 +713,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
"VIDEO" -> {
|
"VIDEO" -> {
|
||||||
val url = intent.getStringExtra("VIDEO");
|
val url = intent.getStringExtra("VIDEO");
|
||||||
navigate(_fragVideoDetail, url);
|
navigateWhenReady(_fragVideoDetail, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
"IMPORT_OPTIONS" -> {
|
"IMPORT_OPTIONS" -> {
|
||||||
|
@ -725,11 +731,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
"Sources" -> {
|
"Sources" -> {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
|
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
|
||||||
navigate(_fragMainSources);
|
navigateWhenReady(_fragMainSources);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
"BROWSE_PLUGINS" -> {
|
"BROWSE_PLUGINS" -> {
|
||||||
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
navigateWhenReady(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
|
||||||
Pair("grayjay") { req ->
|
Pair("grayjay") { req ->
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
if (it is MainActivity) {
|
if (it is MainActivity) {
|
||||||
|
@ -747,8 +753,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
runBlocking {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
handleUrlAll(targetData)
|
handleUrlAll(targetData)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
|
@ -776,10 +786,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
} else if (url.startsWith("grayjay://video/")) {
|
} else if (url.startsWith("grayjay://video/")) {
|
||||||
val videoUrl = url.substring("grayjay://video/".length);
|
val videoUrl = url.substring("grayjay://video/".length);
|
||||||
navigate(_fragVideoDetail, videoUrl);
|
navigateWhenReady(_fragVideoDetail, videoUrl);
|
||||||
} else if (url.startsWith("grayjay://channel/")) {
|
} else if (url.startsWith("grayjay://channel/")) {
|
||||||
val channelUrl = url.substring("grayjay://channel/".length);
|
val channelUrl = url.substring("grayjay://channel/".length);
|
||||||
navigate(_fragMainChannel, channelUrl);
|
navigateWhenReady(_fragMainChannel, channelUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -847,27 +857,27 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
Logger.i(TAG, "handleUrl(url=$url) on IO");
|
||||||
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
if (StatePlatform.instance.hasEnabledVideoClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
Logger.i(TAG, "handleUrl(url=$url) found video client");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (position > 0)
|
if (position > 0)
|
||||||
navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
|
navigateWhenReady(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
|
||||||
else
|
else
|
||||||
navigate(_fragVideoDetail, url);
|
navigateWhenReady(_fragVideoDetail, url);
|
||||||
|
|
||||||
_fragVideoDetail.maximizeVideoDetail(true);
|
_fragVideoDetail.maximizeVideoDetail(true);
|
||||||
}
|
}
|
||||||
return@withContext true;
|
return@withContext true;
|
||||||
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
} else if (StatePlatform.instance.hasEnabledChannelClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
Logger.i(TAG, "handleUrl(url=$url) found channel client");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
navigate(_fragMainChannel, url);
|
navigateWhenReady(_fragMainChannel, url);
|
||||||
delay(100);
|
delay(100);
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
return@withContext true;
|
return@withContext true;
|
||||||
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
} else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) {
|
||||||
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
Logger.i(TAG, "handleUrl(url=$url) found playlist client");
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
navigate(_fragMainRemotePlaylist, url);
|
navigateWhenReady(_fragMainRemotePlaylist, url);
|
||||||
delay(100);
|
delay(100);
|
||||||
_fragVideoDetail.minimizeVideoDetail();
|
_fragVideoDetail.minimizeVideoDetail();
|
||||||
};
|
};
|
||||||
|
@ -1094,6 +1104,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
return fragCurrent is T;
|
return fragCurrent is T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun navigateWhenReady(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
|
||||||
|
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||||||
|
navigate(segment, parameter, withHistory, isBack)
|
||||||
|
} else {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
lifecycle.withStateAtLeast(Lifecycle.State.RESUMED) {
|
||||||
|
navigate(segment, parameter, withHistory, isBack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate takes a MainFragment, and makes them the current main visible view
|
* Navigate takes a MainFragment, and makes them the current main visible view
|
||||||
* A parameter can be provided which becomes available in the onShow of said fragment
|
* A parameter can be provided which becomes available in the onShow of said fragment
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
@ -100,12 +101,18 @@ class SyncHomeActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateSync.instance.confirmStarted(this, {
|
StateSync.instance.confirmStarted(this, onStarted = {
|
||||||
StateSync.instance.showFailedToBindDialogIfNecessary(this@SyncHomeActivity)
|
if (StateSync.instance.syncService?.serverSocketFailedToStart == true) {
|
||||||
}, {
|
UIDialogs.toast(this, "Server socket failed to start, is the port in use?", true)
|
||||||
|
}
|
||||||
|
if (StateSync.instance.syncService?.relayConnected == false) {
|
||||||
|
UIDialogs.toast(this, "Not connected to relay, remote connections will work.", false)
|
||||||
|
}
|
||||||
|
if (StateSync.instance.syncService?.serverSocketStarted == false) {
|
||||||
|
UIDialogs.toast(this, "Listener not started, local connections will not work.", false)
|
||||||
|
}
|
||||||
|
}, onNotStarted = {
|
||||||
finish()
|
finish()
|
||||||
}, {
|
|
||||||
StateSync.instance.showFailedToBindDialogIfNecessary(this@SyncHomeActivity)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,9 @@ import com.futo.platformplayer.toHexString
|
||||||
import com.futo.platformplayer.toInetAddress
|
import com.futo.platformplayer.toInetAddress
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
@ -56,6 +58,10 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
private var _mediaSessionId: Int? = null;
|
private var _mediaSessionId: Int? = null;
|
||||||
private var _thread: Thread? = null;
|
private var _thread: Thread? = null;
|
||||||
private var _pingThread: Thread? = null;
|
private var _pingThread: Thread? = null;
|
||||||
|
private var _launchRetries = 0
|
||||||
|
private val MAX_LAUNCH_RETRIES = 3
|
||||||
|
private var _lastLaunchTime_ms = 0L
|
||||||
|
private var _retryJob: Job? = null
|
||||||
|
|
||||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
@ -229,6 +235,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
launchObject.put("appId", "CC1AD845");
|
launchObject.put("appId", "CC1AD845");
|
||||||
launchObject.put("requestId", _requestId++);
|
launchObject.put("requestId", _requestId++);
|
||||||
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.receiver", launchObject.toString());
|
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.receiver", launchObject.toString());
|
||||||
|
_lastLaunchTime_ms = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getStatus() {
|
private fun getStatus() {
|
||||||
|
@ -268,6 +275,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
_contentType = null;
|
_contentType = null;
|
||||||
_streamType = null;
|
_streamType = null;
|
||||||
_sessionId = null;
|
_sessionId = null;
|
||||||
|
_launchRetries = 0
|
||||||
_transportId = null;
|
_transportId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,6 +290,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
|
|
||||||
_started = true;
|
_started = true;
|
||||||
_sessionId = null;
|
_sessionId = null;
|
||||||
|
_launchRetries = 0
|
||||||
_mediaSessionId = null;
|
_mediaSessionId = null;
|
||||||
|
|
||||||
Logger.i(TAG, "Starting...");
|
Logger.i(TAG, "Starting...");
|
||||||
|
@ -393,7 +402,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
try {
|
try {
|
||||||
val inputStream = _inputStream ?: break;
|
val inputStream = _inputStream ?: break;
|
||||||
|
|
||||||
synchronized(_inputStreamLock)
|
val message = synchronized(_inputStreamLock)
|
||||||
{
|
{
|
||||||
Log.d(TAG, "Receiving next packet...");
|
Log.d(TAG, "Receiving next packet...");
|
||||||
val b1 = inputStream.readUnsignedByte();
|
val b1 = inputStream.readUnsignedByte();
|
||||||
|
@ -405,7 +414,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
if (size > buffer.size) {
|
if (size > buffer.size) {
|
||||||
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
Logger.w(TAG, "Skipping packet that is too large $size bytes.")
|
||||||
inputStream.skip(size.toLong());
|
inputStream.skip(size.toLong());
|
||||||
return@synchronized
|
return@synchronized null
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
Log.d(TAG, "Received header indicating $size bytes. Waiting for message.");
|
||||||
|
@ -414,15 +423,19 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
//TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
|
||||||
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
|
||||||
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
|
||||||
val message = ChromeCast.CastMessage.parseFrom(messageBytes);
|
val msg = ChromeCast.CastMessage.parseFrom(messageBytes);
|
||||||
if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
if (msg.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
|
||||||
Logger.i(TAG, "Received message: $message");
|
Logger.i(TAG, "Received message: $msg");
|
||||||
|
}
|
||||||
|
return@synchronized msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
try {
|
try {
|
||||||
handleMessage(message);
|
handleMessage(message);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to handle message.", e);
|
Logger.w(TAG, "Failed to handle message.", e);
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: java.net.SocketException) {
|
} catch (e: java.net.SocketException) {
|
||||||
|
@ -512,6 +525,7 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
if (_sessionId == null) {
|
if (_sessionId == null) {
|
||||||
connectionState = CastConnectionState.CONNECTED;
|
connectionState = CastConnectionState.CONNECTED;
|
||||||
_sessionId = applicationUpdate.getString("sessionId");
|
_sessionId = applicationUpdate.getString("sessionId");
|
||||||
|
_launchRetries = 0
|
||||||
|
|
||||||
val transportId = applicationUpdate.getString("transportId");
|
val transportId = applicationUpdate.getString("transportId");
|
||||||
connectMediaChannel(transportId);
|
connectMediaChannel(transportId);
|
||||||
|
@ -526,21 +540,40 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionIsRunning) {
|
if (!sessionIsRunning) {
|
||||||
_sessionId = null;
|
if (System.currentTimeMillis() - _lastLaunchTime_ms > 5000) {
|
||||||
_mediaSessionId = null;
|
_sessionId = null
|
||||||
setTime(0.0);
|
_mediaSessionId = null
|
||||||
_transportId = null;
|
setTime(0.0)
|
||||||
Logger.w(TAG, "Session not found.");
|
_transportId = null
|
||||||
|
|
||||||
if (_launching) {
|
if (_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
||||||
Logger.i(TAG, "Player not found, launching.");
|
Logger.i(TAG, "No player yet; attempting launch #${_launchRetries + 1}")
|
||||||
launchPlayer();
|
_launchRetries++
|
||||||
|
launchPlayer()
|
||||||
|
} else if (!_launching && _launchRetries < MAX_LAUNCH_RETRIES) {
|
||||||
|
// Maybe the first GET_STATUS came back empty; still try launching
|
||||||
|
Logger.i(TAG, "Player not found; triggering launch #${_launchRetries + 1}")
|
||||||
|
_launching = true
|
||||||
|
_launchRetries++
|
||||||
|
launchPlayer()
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Player not found, disconnecting.");
|
Logger.e(TAG, "Player not found after $_launchRetries attempts; giving up.")
|
||||||
stop();
|
Logger.i(TAG, "Unable to start media receiver on device")
|
||||||
|
stop()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_launching = false;
|
if (_retryJob == null) {
|
||||||
|
Logger.i(TAG, "Scheduled retry job over 5 seconds")
|
||||||
|
_retryJob = _scopeIO?.launch(Dispatchers.IO) {
|
||||||
|
delay(5000)
|
||||||
|
getStatus()
|
||||||
|
_retryJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_launching = false
|
||||||
|
_launchRetries = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val volume = status.getJSONObject("volume");
|
val volume = status.getJSONObject("volume");
|
||||||
|
@ -582,6 +615,8 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
if (message.sourceId == "receiver-0") {
|
if (message.sourceId == "receiver-0") {
|
||||||
Logger.i(TAG, "Close received.");
|
Logger.i(TAG, "Close received.");
|
||||||
stop();
|
stop();
|
||||||
|
} else if (_transportId == message.sourceId) {
|
||||||
|
throw Exception("Transport id closed.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -616,6 +651,9 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
localAddress = null;
|
localAddress = null;
|
||||||
_started = false;
|
_started = false;
|
||||||
|
|
||||||
|
_retryJob?.cancel()
|
||||||
|
_retryJob = null
|
||||||
|
|
||||||
val socket = _socket;
|
val socket = _socket;
|
||||||
val scopeIO = _scopeIO;
|
val scopeIO = _scopeIO;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ import android.os.Build
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
import java.net.Inet4Address
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
@ -41,6 +43,7 @@ import com.futo.platformplayer.builders.DashBuilder
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
|
import com.futo.platformplayer.findPreferredAddress
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
|
@ -55,9 +58,11 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.net.Inet6Address
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
import java.util.Collections
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class StateCasting {
|
class StateCasting {
|
||||||
|
@ -483,7 +488,7 @@ class StateCasting {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
if (videoSource is IVideoUrlSource) {
|
if (videoSource is IVideoUrlSource) {
|
||||||
|
@ -578,7 +583,7 @@ class StateCasting {
|
||||||
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
val videoUrl = url + videoPath;
|
val videoUrl = url + videoPath;
|
||||||
|
@ -597,7 +602,7 @@ class StateCasting {
|
||||||
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val audioPath = "/audio-${id}"
|
val audioPath = "/audio-${id}"
|
||||||
val audioUrl = url + audioPath;
|
val audioUrl = url + audioPath;
|
||||||
|
@ -616,7 +621,7 @@ class StateCasting {
|
||||||
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
val ad = activeDevice ?: return listOf()
|
val ad = activeDevice ?: return listOf()
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}"
|
val url = getLocalUrl(ad)
|
||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
|
@ -712,7 +717,7 @@ class StateCasting {
|
||||||
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
|
@ -762,7 +767,7 @@ class StateCasting {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
|
@ -827,7 +832,7 @@ class StateCasting {
|
||||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||||
|
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
|
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
|
@ -997,7 +1002,7 @@ class StateCasting {
|
||||||
|
|
||||||
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
|
@ -1127,7 +1132,7 @@ class StateCasting {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
|
@ -1213,6 +1218,15 @@ class StateCasting {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getLocalUrl(ad: CastingDevice): String {
|
||||||
|
var address = ad.localAddress!!
|
||||||
|
if (address is Inet6Address && address.isLinkLocalAddress) {
|
||||||
|
address = findPreferredAddress() ?: address
|
||||||
|
Logger.i(TAG, "Selected casting address: $address")
|
||||||
|
}
|
||||||
|
return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
@ -1220,7 +1234,7 @@ class StateCasting {
|
||||||
cleanExecutors()
|
cleanExecutors()
|
||||||
_castServer.removeAllHandlers("castDashRaw")
|
_castServer.removeAllHandlers("castDashRaw")
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
val url = getLocalUrl(ad);
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
|
|
|
@ -176,6 +176,11 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSearchViewVisibility() {
|
private fun updateSearchViewVisibility() {
|
||||||
|
if (subType != null) {
|
||||||
|
_searchView?.visibility = View.GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val client = _channel?.id?.pluginId?.let { StatePlatform.instance.getClientOrNull(it) }
|
val client = _channel?.id?.pluginId?.let { StatePlatform.instance.getClientOrNull(it) }
|
||||||
Logger.i(TAG, "_searchView.visible = ${client?.capabilities?.hasSearchChannelContents == true}")
|
Logger.i(TAG, "_searchView.visible = ${client?.capabilities?.hasSearchChannelContents == true}")
|
||||||
_searchView?.visibility = if (client?.capabilities?.hasSearchChannelContents == true) View.VISIBLE else View.GONE
|
_searchView?.visibility = if (client?.capabilities?.hasSearchChannelContents == true) View.VISIBLE else View.GONE
|
||||||
|
|
|
@ -412,24 +412,12 @@ class StateApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.instance.synchronization.enabled) {
|
if (Settings.instance.synchronization.enabled) {
|
||||||
StateSync.instance.start(context, {
|
StateSync.instance.start(context)
|
||||||
try {
|
|
||||||
UIDialogs.toast("Failed to start sync, port in use")
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
//Ignored
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsActivityClosed.subscribe {
|
settingsActivityClosed.subscribe {
|
||||||
if (Settings.instance.synchronization.enabled) {
|
if (Settings.instance.synchronization.enabled) {
|
||||||
StateSync.instance.start(context, {
|
StateSync.instance.start(context)
|
||||||
try {
|
|
||||||
UIDialogs.toast("Failed to start sync, port in use")
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
//Ignored
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
StateSync.instance.stop()
|
StateSync.instance.stop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ class StateSync {
|
||||||
val deviceRemoved: Event1<String> = Event1()
|
val deviceRemoved: Event1<String> = Event1()
|
||||||
val deviceUpdatedOrAdded: Event2<String, SyncSession> = Event2()
|
val deviceUpdatedOrAdded: Event2<String, SyncSession> = Event2()
|
||||||
|
|
||||||
fun start(context: Context, onServerBindFail: () -> Unit) {
|
fun start(context: Context) {
|
||||||
if (syncService != null) {
|
if (syncService != null) {
|
||||||
Logger.i(TAG, "Already started.")
|
Logger.i(TAG, "Already started.")
|
||||||
return
|
return
|
||||||
|
@ -150,24 +150,14 @@ class StateSync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
syncService?.start(context, onServerBindFail)
|
syncService?.start(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showFailedToBindDialogIfNecessary(context: Context) {
|
fun confirmStarted(context: Context, onStarted: () -> Unit, onNotStarted: () -> Unit) {
|
||||||
if (syncService?.serverSocketFailedToStart == true && Settings.instance.synchronization.localConnections) {
|
|
||||||
try {
|
|
||||||
UIDialogs.showDialogOk(context, R.drawable.ic_warning, "Local discovery unavailable, port was in use")
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
//Ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun confirmStarted(context: Context, onStarted: () -> Unit, onNotStarted: () -> Unit, onServerBindFail: () -> Unit) {
|
|
||||||
if (syncService == null) {
|
if (syncService == null) {
|
||||||
UIDialogs.showConfirmationDialog(context, "Sync has not been enabled yet, would you like to enable sync?", {
|
UIDialogs.showConfirmationDialog(context, "Sync has not been enabled yet, would you like to enable sync?", {
|
||||||
Settings.instance.synchronization.enabled = true
|
Settings.instance.synchronization.enabled = true
|
||||||
start(context, onServerBindFail)
|
start(context)
|
||||||
Settings.instance.save()
|
Settings.instance.save()
|
||||||
onStarted.invoke()
|
onStarted.invoke()
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -72,6 +72,8 @@ class SyncService(
|
||||||
private val _lastConnectTimesMdns: MutableMap<String, Long> = mutableMapOf()
|
private val _lastConnectTimesMdns: MutableMap<String, Long> = mutableMapOf()
|
||||||
private val _lastConnectTimesIp: MutableMap<String, Long> = mutableMapOf()
|
private val _lastConnectTimesIp: MutableMap<String, Long> = mutableMapOf()
|
||||||
var serverSocketFailedToStart = false
|
var serverSocketFailedToStart = false
|
||||||
|
var serverSocketStarted = false
|
||||||
|
var relayConnected = false
|
||||||
//TODO: Should sync mdns and casting mdns be merged?
|
//TODO: Should sync mdns and casting mdns be merged?
|
||||||
//TODO: Decrease interval that devices are updated
|
//TODO: Decrease interval that devices are updated
|
||||||
//TODO: Send less data
|
//TODO: Send less data
|
||||||
|
@ -212,7 +214,7 @@ class SyncService(
|
||||||
var onData: ((SyncSession, UByte, UByte, ByteBuffer) -> Unit)? = null
|
var onData: ((SyncSession, UByte, UByte, ByteBuffer) -> Unit)? = null
|
||||||
var authorizePrompt: ((String, (Boolean) -> Unit) -> Unit)? = null
|
var authorizePrompt: ((String, (Boolean) -> Unit) -> Unit)? = null
|
||||||
|
|
||||||
fun start(context: Context, onServerBindFail: (() -> Unit)? = null) {
|
fun start(context: Context) {
|
||||||
if (_started) {
|
if (_started) {
|
||||||
Logger.i(TAG, "Already started.")
|
Logger.i(TAG, "Already started.")
|
||||||
return
|
return
|
||||||
|
@ -273,10 +275,12 @@ class SyncService(
|
||||||
|
|
||||||
Logger.i(TAG, "Sync key pair initialized (public key = $publicKey)")
|
Logger.i(TAG, "Sync key pair initialized (public key = $publicKey)")
|
||||||
|
|
||||||
|
serverSocketStarted = false
|
||||||
if (settings.bindListener) {
|
if (settings.bindListener) {
|
||||||
startListener(onServerBindFail)
|
startListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relayConnected = false
|
||||||
if (settings.relayEnabled) {
|
if (settings.relayEnabled) {
|
||||||
startRelayLoop()
|
startRelayLoop()
|
||||||
}
|
}
|
||||||
|
@ -286,13 +290,15 @@ class SyncService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startListener(onServerBindFail: (() -> Unit)? = null) {
|
private fun startListener() {
|
||||||
serverSocketFailedToStart = false
|
serverSocketFailedToStart = false
|
||||||
|
serverSocketStarted = false
|
||||||
_thread = Thread {
|
_thread = Thread {
|
||||||
try {
|
try {
|
||||||
val serverSocket = ServerSocket(settings.listenerPort)
|
val serverSocket = ServerSocket(settings.listenerPort)
|
||||||
_serverSocket = serverSocket
|
_serverSocket = serverSocket
|
||||||
|
|
||||||
|
serverSocketStarted = true
|
||||||
Log.i(TAG, "Running on port ${settings.listenerPort} (TCP)")
|
Log.i(TAG, "Running on port ${settings.listenerPort} (TCP)")
|
||||||
|
|
||||||
while (_started) {
|
while (_started) {
|
||||||
|
@ -300,10 +306,12 @@ class SyncService(
|
||||||
val session = createSocketSession(socket, true)
|
val session = createSocketSession(socket, true)
|
||||||
session.startAsResponder()
|
session.startAsResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverSocketStarted = false
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to bind server socket to port ${settings.listenerPort}", e)
|
Logger.e(TAG, "Failed to bind server socket to port ${settings.listenerPort}", e)
|
||||||
serverSocketFailedToStart = true
|
serverSocketFailedToStart = true
|
||||||
onServerBindFail?.invoke()
|
serverSocketStarted = false
|
||||||
}
|
}
|
||||||
}.apply { start() }
|
}.apply { start() }
|
||||||
}
|
}
|
||||||
|
@ -386,13 +394,16 @@ class SyncService(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startRelayLoop() {
|
private fun startRelayLoop() {
|
||||||
|
relayConnected = false
|
||||||
_threadRelay = Thread {
|
_threadRelay = Thread {
|
||||||
|
try {
|
||||||
var backoffs: Array<Long> = arrayOf(1000, 5000, 10000, 20000)
|
var backoffs: Array<Long> = arrayOf(1000, 5000, 10000, 20000)
|
||||||
var backoffIndex = 0;
|
var backoffIndex = 0;
|
||||||
|
|
||||||
while (_started) {
|
while (_started) {
|
||||||
try {
|
try {
|
||||||
Log.i(TAG, "Starting relay session...")
|
Log.i(TAG, "Starting relay session...")
|
||||||
|
relayConnected = false
|
||||||
|
|
||||||
var socketClosed = false;
|
var socketClosed = false;
|
||||||
val socket = Socket(relayServer, 9000)
|
val socket = Socket(relayServer, 9000)
|
||||||
|
@ -400,22 +411,38 @@ class SyncService(
|
||||||
(socket.remoteSocketAddress as InetSocketAddress).address.hostAddress!!,
|
(socket.remoteSocketAddress as InetSocketAddress).address.hostAddress!!,
|
||||||
keyPair!!,
|
keyPair!!,
|
||||||
socket,
|
socket,
|
||||||
isHandshakeAllowed = { linkType, syncSocketSession, publicKey, pairingCode, appId -> isHandshakeAllowed(linkType, syncSocketSession, publicKey, pairingCode, appId) },
|
isHandshakeAllowed = { linkType, syncSocketSession, publicKey, pairingCode, appId ->
|
||||||
|
isHandshakeAllowed(
|
||||||
|
linkType,
|
||||||
|
syncSocketSession,
|
||||||
|
publicKey,
|
||||||
|
pairingCode,
|
||||||
|
appId
|
||||||
|
)
|
||||||
|
},
|
||||||
onNewChannel = { _, c ->
|
onNewChannel = { _, c ->
|
||||||
val remotePublicKey = c.remotePublicKey
|
val remotePublicKey = c.remotePublicKey
|
||||||
if (remotePublicKey == null) {
|
if (remotePublicKey == null) {
|
||||||
Log.e(TAG, "Remote public key should never be null in onNewChannel.")
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"Remote public key should never be null in onNewChannel."
|
||||||
|
)
|
||||||
return@SyncSocketSession
|
return@SyncSocketSession
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "New channel established from relay (pk: '$remotePublicKey').")
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"New channel established from relay (pk: '$remotePublicKey')."
|
||||||
|
)
|
||||||
|
|
||||||
var session: SyncSession?
|
var session: SyncSession?
|
||||||
synchronized(_sessions) {
|
synchronized(_sessions) {
|
||||||
session = _sessions[remotePublicKey]
|
session = _sessions[remotePublicKey]
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
val remoteDeviceName = database.getDeviceName(remotePublicKey)
|
val remoteDeviceName =
|
||||||
session = createNewSyncSession(remotePublicKey, remoteDeviceName)
|
database.getDeviceName(remotePublicKey)
|
||||||
|
session =
|
||||||
|
createNewSyncSession(remotePublicKey, remoteDeviceName)
|
||||||
_sessions[remotePublicKey] = session!!
|
_sessions[remotePublicKey] = session!!
|
||||||
}
|
}
|
||||||
session!!.addChannel(c)
|
session!!.addChannel(c)
|
||||||
|
@ -438,23 +465,57 @@ class SyncService(
|
||||||
Thread {
|
Thread {
|
||||||
try {
|
try {
|
||||||
while (_started && !socketClosed) {
|
while (_started && !socketClosed) {
|
||||||
val unconnectedAuthorizedDevices = database.getAllAuthorizedDevices()?.filter { !isConnected(it) }?.toTypedArray() ?: arrayOf()
|
val unconnectedAuthorizedDevices =
|
||||||
relaySession.publishConnectionInformation(unconnectedAuthorizedDevices, settings.listenerPort, settings.relayConnectDirect, false, false, settings.relayConnectRelayed)
|
database.getAllAuthorizedDevices()
|
||||||
|
?.filter { !isConnected(it) }?.toTypedArray()
|
||||||
|
?: arrayOf()
|
||||||
|
relaySession.publishConnectionInformation(
|
||||||
|
unconnectedAuthorizedDevices,
|
||||||
|
settings.listenerPort,
|
||||||
|
settings.relayConnectDirect,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
settings.relayConnectRelayed
|
||||||
|
)
|
||||||
|
|
||||||
Logger.v(TAG, "Requesting ${unconnectedAuthorizedDevices.size} devices connection information")
|
Logger.v(
|
||||||
val connectionInfos = runBlocking { relaySession.requestBulkConnectionInfo(unconnectedAuthorizedDevices) }
|
TAG,
|
||||||
Logger.v(TAG, "Received ${connectionInfos.size} devices connection information")
|
"Requesting ${unconnectedAuthorizedDevices.size} devices connection information"
|
||||||
|
)
|
||||||
|
val connectionInfos = runBlocking {
|
||||||
|
relaySession.requestBulkConnectionInfo(
|
||||||
|
unconnectedAuthorizedDevices
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Logger.v(
|
||||||
|
TAG,
|
||||||
|
"Received ${connectionInfos.size} devices connection information"
|
||||||
|
)
|
||||||
|
|
||||||
for ((targetKey, connectionInfo) in connectionInfos) {
|
for ((targetKey, connectionInfo) in connectionInfos) {
|
||||||
val potentialLocalAddresses = connectionInfo.ipv4Addresses
|
val potentialLocalAddresses =
|
||||||
|
connectionInfo.ipv4Addresses
|
||||||
.filter { it != connectionInfo.remoteIp }
|
.filter { it != connectionInfo.remoteIp }
|
||||||
if (connectionInfo.allowLocalDirect && Settings.instance.synchronization.connectLocalDirectThroughRelay) {
|
if (connectionInfo.allowLocalDirect && Settings.instance.synchronization.connectLocalDirectThroughRelay) {
|
||||||
Thread {
|
Thread {
|
||||||
try {
|
try {
|
||||||
Log.v(TAG, "Attempting to connect directly, locally to '$targetKey'.")
|
Log.v(
|
||||||
connect(potentialLocalAddresses.map { it }.toTypedArray(), settings.listenerPort, targetKey, null)
|
TAG,
|
||||||
|
"Attempting to connect directly, locally to '$targetKey'."
|
||||||
|
)
|
||||||
|
connect(
|
||||||
|
potentialLocalAddresses.map { it }
|
||||||
|
.toTypedArray(),
|
||||||
|
settings.listenerPort,
|
||||||
|
targetKey,
|
||||||
|
null
|
||||||
|
)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "Failed to start direct connection using connection info with $targetKey.", e)
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"Failed to start direct connection using connection info with $targetKey.",
|
||||||
|
e
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
@ -469,10 +530,23 @@ class SyncService(
|
||||||
|
|
||||||
if (connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) {
|
if (connectionInfo.allowRemoteRelayed && Settings.instance.synchronization.connectThroughRelay) {
|
||||||
try {
|
try {
|
||||||
Logger.v(TAG, "Attempting relayed connection with '$targetKey'.")
|
Logger.v(
|
||||||
runBlocking { relaySession.startRelayedChannel(targetKey, appId, null) }
|
TAG,
|
||||||
|
"Attempting relayed connection with '$targetKey'."
|
||||||
|
)
|
||||||
|
runBlocking {
|
||||||
|
relaySession.startRelayedChannel(
|
||||||
|
targetKey,
|
||||||
|
appId,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to start relayed channel with $targetKey.", e)
|
Logger.e(
|
||||||
|
TAG,
|
||||||
|
"Failed to start relayed channel with $targetKey.",
|
||||||
|
e
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -491,17 +565,22 @@ class SyncService(
|
||||||
override val isAuthorized: Boolean get() = true
|
override val isAuthorized: Boolean get() = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relayConnected = true
|
||||||
_relaySession!!.runAsInitiator(relayPublicKey, appId, null)
|
_relaySession!!.runAsInitiator(relayPublicKey, appId, null)
|
||||||
|
|
||||||
Log.i(TAG, "Started relay session.")
|
Log.i(TAG, "Started relay session.")
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "Relay session failed.", e)
|
Log.e(TAG, "Relay session failed.", e)
|
||||||
} finally {
|
} finally {
|
||||||
|
relayConnected = false
|
||||||
_relaySession?.stop()
|
_relaySession?.stop()
|
||||||
_relaySession = null
|
_relaySession = null
|
||||||
Thread.sleep(backoffs[min(backoffs.size - 1, backoffIndex++)])
|
Thread.sleep(backoffs[min(backoffs.size - 1, backoffIndex++)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Log.i(TAG, "Unhandled exception in relay loop.", ex)
|
||||||
|
}
|
||||||
}.apply { start() }
|
}.apply { start() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -529,7 +529,7 @@ class SyncSocketSession {
|
||||||
val isAllowed = publicKey != _localPublicKey && (_isHandshakeAllowed?.invoke(LinkType.Relayed, this, publicKey, pairingCode, appId) ?: true)
|
val isAllowed = publicKey != _localPublicKey && (_isHandshakeAllowed?.invoke(LinkType.Relayed, this, publicKey, pairingCode, appId) ?: true)
|
||||||
if (!isAllowed) {
|
if (!isAllowed) {
|
||||||
val rp = ByteBuffer.allocate(16).order(ByteOrder.LITTLE_ENDIAN)
|
val rp = ByteBuffer.allocate(16).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
rp.putInt(2) // Status code for not allowed
|
rp.putInt(7) // Status code for not allowed
|
||||||
rp.putLong(connectionId)
|
rp.putLong(connectionId)
|
||||||
rp.putInt(requestId)
|
rp.putInt(requestId)
|
||||||
rp.rewind()
|
rp.rewind()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue