diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index f5796d4d..1bf1958c 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -725,11 +725,6 @@ class Settings : FragmentedStorageFileJson() { @Serializable(with = FlexibleBooleanSerializer::class) var allowLinkLocalIpv4: Boolean = false; - @AdvancedField - @FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6) - @Serializable(with = FlexibleBooleanSerializer::class) - var experimentalCasting: Boolean = true - /*TODO: Should we have a different casting quality? @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3) @DropdownFieldOptionsId(R.array.preferred_quality_array) diff --git a/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt deleted file mode 100644 index b8ea2df6..00000000 --- a/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt +++ /dev/null @@ -1,330 +0,0 @@ -package com.futo.platformplayer.casting - -import android.os.Looper -import com.futo.platformplayer.api.http.ManagedHttpClient -import com.futo.platformplayer.getConnectedSocket -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.models.CastingDeviceInfo -import com.futo.platformplayer.toInetAddress -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import java.net.InetAddress -import java.util.UUID - -class AirPlayCastingDevice : CastingDeviceLegacy { - //See for more info: https://nto.github.io/AirPlay - - override val protocol: CastProtocolType get() = CastProtocolType.AIRPLAY; - override val isReady: Boolean get() = name != null && addresses != null && addresses?.isNotEmpty() == true && port != 0; - override var usedRemoteAddress: InetAddress? = null; - override var localAddress: InetAddress? = null; - override val canSetVolume: Boolean get() = false; - override val canSetSpeed: Boolean get() = true; - - var addresses: Array? = null; - var port: Int = 0; - - private var _scopeIO: CoroutineScope? = null; - private var _started: Boolean = false; - private var _sessionId: String? = null; - private val _client = ManagedHttpClient(); - - constructor(name: String, addresses: Array, port: Int) : super() { - this.name = name; - this.addresses = addresses; - this.port = port; - } - - constructor(deviceInfo: CastingDeviceInfo) : super() { - this.name = deviceInfo.name; - this.addresses = deviceInfo.addresses.map { a -> a.toInetAddress() }.filterNotNull().toTypedArray(); - this.port = deviceInfo.port; - } - - override fun getAddresses(): List { - return addresses?.toList() ?: listOf(); - } - - override fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double, speed: Double?) { - if (invokeInIOScopeIfRequired({ loadVideo(streamType, contentType, contentId, resumePosition, duration, speed) })) { - return; - } - - Logger.i(FCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)"); - - setTime(resumePosition); - setDuration(duration); - if (resumePosition > 0.0) { - val pos = resumePosition / duration; - Logger.i(TAG, "resumePosition: $resumePosition, duration: ${duration}, pos: $pos") - post("play", "text/parameters", "Content-Location: $contentId\r\nStart-Position: $pos"); - } else { - post("play", "text/parameters", "Content-Location: $contentId\r\nStart-Position: 0"); - } - - if (speed != null) { - changeSpeed(speed) - } - } - - override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) { - throw NotImplementedError(); - } - - override fun seekVideo(timeSeconds: Double) { - if (invokeInIOScopeIfRequired({ seekVideo(timeSeconds) })) { - return; - } - - post("scrub?position=${timeSeconds}"); - } - - override fun resumeVideo() { - if (invokeInIOScopeIfRequired(::resumeVideo)) { - return; - } - - isPlaying = true; - post("rate?value=1.000000"); - } - - override fun pauseVideo() { - if (invokeInIOScopeIfRequired(::pauseVideo)) { - return; - } - - isPlaying = false; - post("rate?value=0.000000"); - } - - override fun stopVideo() { - if (invokeInIOScopeIfRequired(::stopVideo)) { - return; - } - - post("stop"); - } - - override fun stopCasting() { - if (invokeInIOScopeIfRequired(::stopCasting)) { - return; - } - - post("stop"); - stop(); - } - - override fun start() { - val adrs = addresses ?: return; - if (_started) { - return; - } - - _started = true; - _scopeIO?.cancel(); - _scopeIO = CoroutineScope(Dispatchers.IO); - - Logger.i(TAG, "Starting..."); - - _scopeIO?.launch { - try { - connectionState = CastConnectionState.CONNECTING; - - while (_scopeIO?.isActive == true) { - try { - val connectedSocket = getConnectedSocket(adrs.toList(), port); - if (connectedSocket == null) { - delay(1000); - continue; - } - - usedRemoteAddress = connectedSocket.inetAddress; - localAddress = connectedSocket.localAddress; - connectedSocket.close(); - _sessionId = UUID.randomUUID().toString(); - break; - } catch (e: Throwable) { - Logger.w(TAG, "Failed to get setup initial connection to AirPlay device.", e) - delay(1000); - } - } - - while (_scopeIO?.isActive == true) { - try { - val progressInfo = getProgress(); - if (progressInfo == null) { - connectionState = CastConnectionState.CONNECTING; - Logger.i(TAG, "Failed to retrieve progress from AirPlay device."); - delay(1000); - continue; - } - - connectionState = CastConnectionState.CONNECTED; - - val progressIndex = progressInfo.lowercase().indexOf("position: "); - if (progressIndex == -1) { - delay(1000); - continue; - } - - val progress = progressInfo.substring(progressIndex + "position: ".length).toDoubleOrNull() ?: continue; - setTime(progress); - - val durationIndex = progressInfo.lowercase().indexOf("duration: "); - if (durationIndex == -1) { - delay(1000); - continue; - } - - val duration = progressInfo.substring(durationIndex + "duration: ".length).toDoubleOrNull() ?: continue; - setDuration(duration); - delay(1000); - } catch (e: Throwable) { - Logger.w(TAG, "Failed to get server info from AirPlay device.", e) - } - } - } catch (e: Throwable) { - Logger.w(TAG, "Failed to setup AirPlay device connection.", e) - } - }; - - Logger.i(TAG, "Started."); - } - - override fun stop() { - Logger.i(TAG, "Stopping..."); - connectionState = CastConnectionState.DISCONNECTED; - - usedRemoteAddress = null; - localAddress = null; - _started = false; - _scopeIO?.cancel(); - _scopeIO = null; - } - - override fun changeSpeed(speed: Double) { - setSpeed(speed) - post("rate?value=$speed") - } - - override fun getDeviceInfo(): CastingDeviceInfo { - return CastingDeviceInfo(name!!, CastProtocolType.AIRPLAY, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port); - } - - private fun getProgress(): String? { - val info = get("scrub"); - Logger.i(TAG, "Progress: ${info ?: "null"}"); - return info; - } - - private fun getPlaybackInfo(): String? { - val playbackInfo = get("playback-info"); - Logger.i(TAG, "Playback info: ${playbackInfo ?: "null"}"); - return playbackInfo; - } - - private fun getServerInfo(): String? { - val serverInfo = get("server-info"); - Logger.i(TAG, "Server info: ${serverInfo ?: "null"}"); - return serverInfo; - } - - private fun post(path: String): Boolean { - try { - val sessionId = _sessionId ?: return false; - - val headers = hashMapOf( - "X-Apple-Device-ID" to "0xdc2b61a0ce79", - "User-Agent" to "MediaControl/1.0", - "Content-Length" to "0", - "X-Apple-Session-ID" to sessionId - ); - - val url = "http://${usedRemoteAddress}:${port}/${path}"; - - Logger.i(TAG, "POST $url"); - val response = _client.post(url, headers); - if (!response.isOk) { - return false; - } - - return true; - } catch (e: Throwable) { - Logger.w(TAG, "Failed to POST $path"); - return false; - } - } - - private fun post(path: String, contentType: String, body: String): Boolean { - try { - val sessionId = _sessionId ?: return false; - - val headers = hashMapOf( - "X-Apple-Device-ID" to "0xdc2b61a0ce79", - "User-Agent" to "MediaControl/1.0", - "X-Apple-Session-ID" to sessionId, - "Content-Type" to contentType - ); - - val url = "http://${usedRemoteAddress}:${port}/${path}"; - - Logger.i(TAG, "POST $url:\n$body"); - val response = _client.post(url, body, headers); - if (!response.isOk) { - return false; - } - - return true; - } catch (e: Throwable) { - Logger.w(TAG, "Failed to POST $path $body"); - return false; - } - } - - private fun get(path: String): String? { - val sessionId = _sessionId ?: return null; - - try { - val headers = hashMapOf( - "X-Apple-Device-ID" to "0xdc2b61a0ce79", - "Content-Length" to "0", - "User-Agent" to "MediaControl/1.0", - "X-Apple-Session-ID" to sessionId - ); - - val url = "http://${usedRemoteAddress}:${port}/${path}"; - - Logger.i(TAG, "GET $url"); - val response = _client.get(url, headers); - if (!response.isOk) { - return null; - } - - if (response.body == null) { - return null; - } - - return response.body.string(); - } catch (e: Throwable) { - Logger.w(TAG, "Failed to GET $path"); - return null; - } - } - - private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean { - if(Looper.getMainLooper().thread == Thread.currentThread()) { - _scopeIO?.launch { action(); } - return true; - } - - return false; - } - - companion object { - val TAG = "AirPlayCastingDevice"; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt index 084bbb21..35503a44 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt @@ -1,62 +1,289 @@ package com.futo.platformplayer.casting +import android.os.Build +import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.CastingDeviceInfo -import org.fcast.sender_sdk.Metadata +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.fcast.sender_sdk.ApplicationInfo +import org.fcast.sender_sdk.CastingDevice as RsCastingDevice +import org.fcast.sender_sdk.KeyEvent +import org.fcast.sender_sdk.MediaEvent import java.net.InetAddress +import org.fcast.sender_sdk.PlaybackState +import org.fcast.sender_sdk.Source +import org.fcast.sender_sdk.urlFormatIpAddr +import java.net.Inet4Address +import java.net.Inet6Address +import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler; +import org.fcast.sender_sdk.DeviceConnectionState +import org.fcast.sender_sdk.DeviceFeature +import org.fcast.sender_sdk.EventSubscription +import org.fcast.sender_sdk.IpAddr +import org.fcast.sender_sdk.LoadRequest +import org.fcast.sender_sdk.MediaItemEventType +import org.fcast.sender_sdk.Metadata +import org.fcast.sender_sdk.ProtocolType -abstract class CastingDevice { - abstract val isReady: Boolean - abstract val usedRemoteAddress: InetAddress? - abstract val localAddress: InetAddress? - abstract val name: String? - abstract val onConnectionStateChanged: Event1 - abstract val onPlayChanged: Event1 - abstract val onTimeChanged: Event1 - abstract val onDurationChanged: Event1 - abstract val onVolumeChanged: Event1 - abstract val onSpeedChanged: Event1 - abstract val onMediaItemEnd: Event0 - abstract var connectionState: CastConnectionState - abstract val protocolType: CastProtocolType - abstract var isPlaying: Boolean - abstract val expectedCurrentTime: Double - abstract var speed: Double - abstract var time: Double - abstract var duration: Double - abstract var volume: Double - abstract fun canSetVolume(): Boolean - abstract fun canSetSpeed(): Boolean +enum class CastConnectionState { + DISCONNECTED, + CONNECTING, + CONNECTED +} - @Throws - abstract fun resumePlayback() +@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class) +enum class CastProtocolType { + CHROMECAST, + AIRPLAY, + FCAST; - @Throws - abstract fun pausePlayback() + object CastProtocolTypeSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING) - @Throws - abstract fun stopPlayback() + override fun serialize(encoder: Encoder, value: CastProtocolType) { + encoder.encodeString(value.name) + } - @Throws - abstract fun seekTo(timeSeconds: Double) + override fun deserialize(decoder: Decoder): CastProtocolType { + val name = decoder.decodeString() + return when (name) { + "FASTCAST" -> FCAST // Handle the renamed case + else -> CastProtocolType.valueOf(name) + } + } + } +} - @Throws - abstract fun changeVolume(timeSeconds: Double) +private fun ipAddrToInetAddress(addr: IpAddr): InetAddress = when (addr) { + is IpAddr.V4 -> Inet4Address.getByAddress( + byteArrayOf( + addr.o1.toByte(), + addr.o2.toByte(), + addr.o3.toByte(), + addr.o4.toByte() + ) + ) - @Throws - abstract fun changeSpeed(speed: Double) + is IpAddr.V6 -> Inet6Address.getByAddress( + byteArrayOf( + addr.o1.toByte(), + addr.o2.toByte(), + addr.o3.toByte(), + addr.o4.toByte(), + addr.o5.toByte(), + addr.o6.toByte(), + addr.o7.toByte(), + addr.o8.toByte(), + addr.o9.toByte(), + addr.o10.toByte(), + addr.o11.toByte(), + addr.o12.toByte(), + addr.o13.toByte(), + addr.o14.toByte(), + addr.o15.toByte(), + addr.o16.toByte() + ) + ) +} - @Throws - abstract fun connect() +// abstract class CastingDevice { +class CastingDevice(val device: RsCastingDevice) { + // abstract val isReady: Boolean + // abstract val usedRemoteAddress: InetAddress? + // abstract val localAddress: InetAddress? + // abstract val name: String? + // abstract val onConnectionStateChanged: Event1 + // abstract val onPlayChanged: Event1 + // abstract val onTimeChanged: Event1 + // abstract val onDurationChanged: Event1 + // abstract val onVolumeChanged: Event1 + // abstract val onSpeedChanged: Event1 + // abstract val onMediaItemEnd: Event0 + // abstract var connectionState: CastConnectionState + // abstract val protocolType: CastProtocolType + // abstract var isPlaying: Boolean + // abstract val expectedCurrentTime: Double + // abstract var speed: Double + // abstract var time: Double + // abstract var duration: Double + // abstract var volume: Double + // abstract fun canSetVolume(): Boolean + // abstract fun canSetSpeed(): Boolean - @Throws - abstract fun disconnect() - abstract fun getDeviceInfo(): CastingDeviceInfo - abstract fun getAddresses(): List + // @Throws + // abstract fun resumePlayback() - @Throws - abstract fun loadVideo( + // @Throws + // abstract fun pausePlayback() + + // @Throws + // abstract fun stopPlayback() + + // @Throws + // abstract fun seekTo(timeSeconds: Double) + + // @Throws + // abstract fun changeVolume(timeSeconds: Double) + + // @Throws + // abstract fun changeSpeed(speed: Double) + + // @Throws + // abstract fun connect() + + // @Throws + // abstract fun disconnect() + // abstract fun getDeviceInfo(): CastingDeviceInfo + // abstract fun getAddresses(): List + + // @Throws + // abstract fun loadVideo( + // streamType: String, + // contentType: String, + // contentId: String, + // resumePosition: Double, + // duration: Double, + // speed: Double?, + // metadata: Metadata? + // ) + + // @Throws + // fun loadContent( + // contentType: String, + // content: String, + // resumePosition: Double, + // duration: Double, + // speed: Double?, + // metadata: Metadata? + // ) + + // fun ensureThreadStarted() + + class EventHandler : RsDeviceEventHandler { + var onConnectionStateChanged = Event1(); + var onPlayChanged = Event1() + var onTimeChanged = Event1() + var onDurationChanged = Event1() + var onVolumeChanged = Event1() + var onSpeedChanged = Event1() + var onMediaItemEnd = Event0() + + override fun connectionStateChanged(state: DeviceConnectionState) { + onConnectionStateChanged.emit(state) + } + + override fun volumeChanged(volume: Double) { + onVolumeChanged.emit(volume) + } + + override fun timeChanged(time: Double) { + onTimeChanged.emit(time) + } + + override fun playbackStateChanged(state: PlaybackState) { + onPlayChanged.emit(state == PlaybackState.PLAYING) + } + + override fun durationChanged(duration: Double) { + onDurationChanged.emit(duration) + } + + override fun speedChanged(speed: Double) { + onSpeedChanged.emit(speed) + } + + override fun sourceChanged(source: Source) { + // TODO + } + + override fun keyEvent(event: KeyEvent) { + // Unreachable + } + + override fun mediaEvent(event: MediaEvent) { + if (event.type == MediaItemEventType.END) { + onMediaItemEnd.emit() + } + } + + override fun playbackError(message: String) { + Logger.e(TAG, "Playback error: $message") + } + } + + val eventHandler = EventHandler() + val isReady: Boolean + get() = device.isReady() + val name: String + get() = device.name() + var usedRemoteAddress: InetAddress? = null + var localAddress: InetAddress? = null + fun canSetVolume(): Boolean = device.supportsFeature(DeviceFeature.SET_VOLUME) + fun canSetSpeed(): Boolean = device.supportsFeature(DeviceFeature.SET_SPEED) + + val onConnectionStateChanged = + Event1() + val onPlayChanged: Event1 + get() = eventHandler.onPlayChanged + val onTimeChanged: Event1 + get() = eventHandler.onTimeChanged + val onDurationChanged: Event1 + get() = eventHandler.onDurationChanged + val onVolumeChanged: Event1 + get() = eventHandler.onVolumeChanged + val onSpeedChanged: Event1 + get() = eventHandler.onSpeedChanged + val onMediaItemEnd: Event0 + get() = eventHandler.onMediaItemEnd + + fun resumePlayback() = device.resumePlayback() + fun pausePlayback() = device.pausePlayback() + fun stopPlayback() = device.stopPlayback() + fun seekTo(timeSeconds: Double) = device.seek(timeSeconds) + fun changeVolume(newVolume: Double) { + device.changeVolume(newVolume) + volume = newVolume + } + fun changeSpeed(speed: Double) = device.changeSpeed(speed) + fun connect() = device.connect( + ApplicationInfo( + "Grayjay Android", + "${BuildConfig.VERSION_NAME}-${BuildConfig.FLAVOR}", + "${Build.MANUFACTURER} ${Build.MODEL}" + ), + eventHandler, + 1000.toULong() + ) + + fun disconnect() = device.disconnect() + + fun getDeviceInfo(): CastingDeviceInfo { + val info = device.getDeviceInfo() + return CastingDeviceInfo( + info.name, + when (info.protocol) { + ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST + ProtocolType.F_CAST -> CastProtocolType.FCAST + }, + addresses = info.addresses.map { urlFormatIpAddr(it) }.toTypedArray(), + port = info.port.toInt(), + ) + } + + fun getAddresses(): List = device.getAddresses().map { + ipAddrToInetAddress(it) + } + + fun loadVideo( streamType: String, contentType: String, contentId: String, @@ -64,18 +291,107 @@ abstract class CastingDevice { duration: Double, speed: Double?, metadata: Metadata? + ) = device.load( + LoadRequest.Video( + contentType = contentType, + url = contentId, + resumePosition = resumePosition, + speed = speed, + volume = volume, + metadata = metadata, + requestHeaders = null, + ) ) - @Throws - abstract fun loadContent( + fun loadContent( contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?, metadata: Metadata? + ) = device.load( + LoadRequest.Content( + contentType = contentType, + content = content, + resumePosition = resumePosition, + speed = speed, + volume = volume, + metadata = metadata, + requestHeaders = null, + ) ) - abstract fun ensureThreadStarted() -} + var connectionState = CastConnectionState.DISCONNECTED + val protocolType: CastProtocolType + get() = when (device.castingProtocol()) { + ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST + ProtocolType.F_CAST -> CastProtocolType.FCAST + } + var volume: Double = 1.0 + var duration: Double = 0.0 + private var lastTimeChangeTime_ms: Long = 0 + var time: Double = 0.0 + var speed: Double = 0.0 + var isPlaying: Boolean = false + val expectedCurrentTime: Double + get() { + val diff = + if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0; + return time + diff + } + + init { + eventHandler.onConnectionStateChanged.subscribe { newState -> + when (newState) { + is DeviceConnectionState.Connected -> { + if (device.supportsFeature(DeviceFeature.MEDIA_EVENT_SUBSCRIPTION)) { + try { + device.subscribeEvent(EventSubscription.MediaItemEnd) + } catch (e: Exception) { + Logger.e(TAG, "Failed to subscribe to MediaItemEnd events: $e") + } + } + usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr) + localAddress = ipAddrToInetAddress(newState.localAddr) + connectionState = CastConnectionState.CONNECTED + onConnectionStateChanged.emit(CastConnectionState.CONNECTED) + } + + DeviceConnectionState.Connecting, DeviceConnectionState.Reconnecting -> { + connectionState = CastConnectionState.CONNECTING + onConnectionStateChanged.emit(CastConnectionState.CONNECTING) + } + + DeviceConnectionState.Disconnected -> { + connectionState = CastConnectionState.DISCONNECTED + onConnectionStateChanged.emit(CastConnectionState.DISCONNECTED) + } + } + + if (newState == DeviceConnectionState.Disconnected) { + try { + Logger.i(TAG, "Stopping device") + device.disconnect() + } catch (e: Throwable) { + Logger.e(TAG, "Failed to stop device: $e") + } + } + } + eventHandler.onPlayChanged.subscribe { isPlaying = it } + eventHandler.onTimeChanged.subscribe { + lastTimeChangeTime_ms = System.currentTimeMillis() + time = it + } + eventHandler.onDurationChanged.subscribe { duration = it } + eventHandler.onVolumeChanged.subscribe { volume = it } + eventHandler.onSpeedChanged.subscribe { speed = it } + } + + fun ensureThreadStarted() {} + + companion object { + private val TAG = "CastingDeviceExp" + } +} diff --git a/app/src/main/java/com/futo/platformplayer/casting/CastingDeviceExp.kt b/app/src/main/java/com/futo/platformplayer/casting/CastingDeviceExp.kt deleted file mode 100644 index 5e96b2f1..00000000 --- a/app/src/main/java/com/futo/platformplayer/casting/CastingDeviceExp.kt +++ /dev/null @@ -1,289 +0,0 @@ -package com.futo.platformplayer.casting - -import android.os.Build -import com.futo.platformplayer.BuildConfig -import com.futo.platformplayer.constructs.Event0 -import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.models.CastingDeviceInfo -import com.futo.polycentric.core.Event -import org.fcast.sender_sdk.ApplicationInfo -import org.fcast.sender_sdk.KeyEvent -import org.fcast.sender_sdk.MediaEvent -import org.fcast.sender_sdk.PlaybackState -import org.fcast.sender_sdk.Source -import java.net.InetAddress -import org.fcast.sender_sdk.CastingDevice as RsCastingDevice; -import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler; -import org.fcast.sender_sdk.DeviceConnectionState -import org.fcast.sender_sdk.DeviceFeature -import org.fcast.sender_sdk.EventSubscription -import org.fcast.sender_sdk.IpAddr -import org.fcast.sender_sdk.LoadRequest -import org.fcast.sender_sdk.MediaItemEventType -import org.fcast.sender_sdk.Metadata -import org.fcast.sender_sdk.ProtocolType -import org.fcast.sender_sdk.urlFormatIpAddr -import java.net.Inet4Address -import java.net.Inet6Address - -private fun ipAddrToInetAddress(addr: IpAddr): InetAddress = when (addr) { - is IpAddr.V4 -> Inet4Address.getByAddress( - byteArrayOf( - addr.o1.toByte(), - addr.o2.toByte(), - addr.o3.toByte(), - addr.o4.toByte() - ) - ) - - is IpAddr.V6 -> Inet6Address.getByAddress( - byteArrayOf( - addr.o1.toByte(), - addr.o2.toByte(), - addr.o3.toByte(), - addr.o4.toByte(), - addr.o5.toByte(), - addr.o6.toByte(), - addr.o7.toByte(), - addr.o8.toByte(), - addr.o9.toByte(), - addr.o10.toByte(), - addr.o11.toByte(), - addr.o12.toByte(), - addr.o13.toByte(), - addr.o14.toByte(), - addr.o15.toByte(), - addr.o16.toByte() - ) - ) -} - -class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() { - class EventHandler : RsDeviceEventHandler { - var onConnectionStateChanged = Event1(); - var onPlayChanged = Event1() - var onTimeChanged = Event1() - var onDurationChanged = Event1() - var onVolumeChanged = Event1() - var onSpeedChanged = Event1() - var onMediaItemEnd = Event0() - - override fun connectionStateChanged(state: DeviceConnectionState) { - onConnectionStateChanged.emit(state) - } - - override fun volumeChanged(volume: Double) { - onVolumeChanged.emit(volume) - } - - override fun timeChanged(time: Double) { - onTimeChanged.emit(time) - } - - override fun playbackStateChanged(state: PlaybackState) { - onPlayChanged.emit(state == PlaybackState.PLAYING) - } - - override fun durationChanged(duration: Double) { - onDurationChanged.emit(duration) - } - - override fun speedChanged(speed: Double) { - onSpeedChanged.emit(speed) - } - - override fun sourceChanged(source: Source) { - // TODO - } - - override fun keyEvent(event: KeyEvent) { - // Unreachable - } - - override fun mediaEvent(event: MediaEvent) { - if (event.type == MediaItemEventType.END) { - onMediaItemEnd.emit() - } - } - - override fun playbackError(message: String) { - Logger.e(TAG, "Playback error: $message") - } - } - - val eventHandler = EventHandler() - override val isReady: Boolean - get() = device.isReady() - override val name: String - get() = device.name() - override var usedRemoteAddress: InetAddress? = null - override var localAddress: InetAddress? = null - override fun canSetVolume(): Boolean = device.supportsFeature(DeviceFeature.SET_VOLUME) - override fun canSetSpeed(): Boolean = device.supportsFeature(DeviceFeature.SET_SPEED) - - override val onConnectionStateChanged = - Event1() - override val onPlayChanged: Event1 - get() = eventHandler.onPlayChanged - override val onTimeChanged: Event1 - get() = eventHandler.onTimeChanged - override val onDurationChanged: Event1 - get() = eventHandler.onDurationChanged - override val onVolumeChanged: Event1 - get() = eventHandler.onVolumeChanged - override val onSpeedChanged: Event1 - get() = eventHandler.onSpeedChanged - override val onMediaItemEnd: Event0 - get() = eventHandler.onMediaItemEnd - - override fun resumePlayback() = device.resumePlayback() - override fun pausePlayback() = device.pausePlayback() - override fun stopPlayback() = device.stopPlayback() - override fun seekTo(timeSeconds: Double) = device.seek(timeSeconds) - override fun changeVolume(newVolume: Double) { - device.changeVolume(newVolume) - volume = newVolume - } - override fun changeSpeed(speed: Double) = device.changeSpeed(speed) - override fun connect() = device.connect( - ApplicationInfo( - "Grayjay Android", - "${BuildConfig.VERSION_NAME}-${BuildConfig.FLAVOR}", - "${Build.MANUFACTURER} ${Build.MODEL}" - ), - eventHandler, - 1000.toULong() - ) - - override fun disconnect() = device.disconnect() - - override fun getDeviceInfo(): CastingDeviceInfo { - val info = device.getDeviceInfo() - return CastingDeviceInfo( - info.name, - when (info.protocol) { - ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST - ProtocolType.F_CAST -> CastProtocolType.FCAST - }, - addresses = info.addresses.map { urlFormatIpAddr(it) }.toTypedArray(), - port = info.port.toInt(), - ) - } - - override fun getAddresses(): List = device.getAddresses().map { - ipAddrToInetAddress(it) - } - - override fun loadVideo( - streamType: String, - contentType: String, - contentId: String, - resumePosition: Double, - duration: Double, - speed: Double?, - metadata: Metadata? - ) = device.load( - LoadRequest.Video( - contentType = contentType, - url = contentId, - resumePosition = resumePosition, - speed = speed, - volume = volume, - metadata = metadata, - requestHeaders = null, - ) - ) - - override fun loadContent( - contentType: String, - content: String, - resumePosition: Double, - duration: Double, - speed: Double?, - metadata: Metadata? - ) = device.load( - LoadRequest.Content( - contentType = contentType, - content = content, - resumePosition = resumePosition, - speed = speed, - volume = volume, - metadata = metadata, - requestHeaders = null, - ) - ) - - override var connectionState = CastConnectionState.DISCONNECTED - override val protocolType: CastProtocolType - get() = when (device.castingProtocol()) { - ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST - ProtocolType.F_CAST -> CastProtocolType.FCAST - } - override var volume: Double = 1.0 - override var duration: Double = 0.0 - private var lastTimeChangeTime_ms: Long = 0 - override var time: Double = 0.0 - override var speed: Double = 0.0 - override var isPlaying: Boolean = false - - override val expectedCurrentTime: Double - get() { - val diff = - if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0; - return time + diff - } - - init { - eventHandler.onConnectionStateChanged.subscribe { newState -> - when (newState) { - is DeviceConnectionState.Connected -> { - if (device.supportsFeature(DeviceFeature.MEDIA_EVENT_SUBSCRIPTION)) { - try { - device.subscribeEvent(EventSubscription.MediaItemEnd) - } catch (e: Exception) { - Logger.e(TAG, "Failed to subscribe to MediaItemEnd events: $e") - } - } - usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr) - localAddress = ipAddrToInetAddress(newState.localAddr) - connectionState = CastConnectionState.CONNECTED - onConnectionStateChanged.emit(CastConnectionState.CONNECTED) - } - - DeviceConnectionState.Connecting, DeviceConnectionState.Reconnecting -> { - connectionState = CastConnectionState.CONNECTING - onConnectionStateChanged.emit(CastConnectionState.CONNECTING) - } - - DeviceConnectionState.Disconnected -> { - connectionState = CastConnectionState.DISCONNECTED - onConnectionStateChanged.emit(CastConnectionState.DISCONNECTED) - } - } - - if (newState == DeviceConnectionState.Disconnected) { - try { - Logger.i(TAG, "Stopping device") - device.disconnect() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to stop device: $e") - } - } - } - eventHandler.onPlayChanged.subscribe { isPlaying = it } - eventHandler.onTimeChanged.subscribe { - lastTimeChangeTime_ms = System.currentTimeMillis() - time = it - } - eventHandler.onDurationChanged.subscribe { duration = it } - eventHandler.onVolumeChanged.subscribe { volume = it } - eventHandler.onSpeedChanged.subscribe { speed = it } - } - - override fun ensureThreadStarted() {} - - companion object { - private val TAG = "CastingDeviceExp" - } -} diff --git a/app/src/main/java/com/futo/platformplayer/casting/CastingDeviceLegacy.kt b/app/src/main/java/com/futo/platformplayer/casting/CastingDeviceLegacy.kt deleted file mode 100644 index abd27c90..00000000 --- a/app/src/main/java/com/futo/platformplayer/casting/CastingDeviceLegacy.kt +++ /dev/null @@ -1,244 +0,0 @@ -package com.futo.platformplayer.casting - -import com.futo.platformplayer.constructs.Event0 -import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.models.CastingDeviceInfo -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import org.fcast.sender_sdk.Metadata -import java.net.InetAddress - -enum class CastConnectionState { - DISCONNECTED, - CONNECTING, - CONNECTED -} - -@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class) -enum class CastProtocolType { - CHROMECAST, - AIRPLAY, - FCAST; - - object CastProtocolTypeSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: CastProtocolType) { - encoder.encodeString(value.name) - } - - override fun deserialize(decoder: Decoder): CastProtocolType { - val name = decoder.decodeString() - return when (name) { - "FASTCAST" -> FCAST // Handle the renamed case - else -> CastProtocolType.valueOf(name) - } - } - } -} - -abstract class CastingDeviceLegacy { - abstract val protocol: CastProtocolType; - abstract val isReady: Boolean; - abstract var usedRemoteAddress: InetAddress?; - abstract var localAddress: InetAddress?; - abstract val canSetVolume: Boolean; - abstract val canSetSpeed: Boolean; - - var name: String? = null; - var isPlaying: Boolean = false - set(value) { - val changed = value != field; - field = value; - if (changed) { - onPlayChanged.emit(value); - } - }; - - private var lastTimeChangeTime_ms: Long = 0 - var time: Double = 0.0 - private set - - protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) { - if (changeTime_ms > lastTimeChangeTime_ms && value != time) { - time = value - lastTimeChangeTime_ms = changeTime_ms - onTimeChanged.emit(value) - } - } - - private var lastDurationChangeTime_ms: Long = 0 - var duration: Double = 0.0 - private set - - protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) { - if (changeTime_ms > lastDurationChangeTime_ms && value != duration) { - duration = value - lastDurationChangeTime_ms = changeTime_ms - onDurationChanged.emit(value) - } - } - - private var lastVolumeChangeTime_ms: Long = 0 - var volume: Double = 1.0 - private set - - protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) { - if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) { - volume = value - lastVolumeChangeTime_ms = changeTime_ms - onVolumeChanged.emit(value) - } - } - - private var lastSpeedChangeTime_ms: Long = 0 - var speed: Double = 1.0 - private set - - protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) { - if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) { - speed = value - lastSpeedChangeTime_ms = changeTime_ms - onSpeedChanged.emit(value) - } - } - - val expectedCurrentTime: Double - get() { - val diff = - if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0; - return time + diff; - }; - var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED - set(value) { - val changed = value != field; - field = value; - - if (changed) { - onConnectionStateChanged.emit(value); - } - }; - - var onConnectionStateChanged = Event1(); - var onPlayChanged = Event1(); - var onTimeChanged = Event1(); - var onDurationChanged = Event1(); - var onVolumeChanged = Event1(); - var onSpeedChanged = Event1(); - - abstract fun stopCasting(); - - abstract fun seekVideo(timeSeconds: Double); - abstract fun stopVideo(); - abstract fun pauseVideo(); - abstract fun resumeVideo(); - abstract fun loadVideo( - streamType: String, - contentType: String, - contentId: String, - resumePosition: Double, - duration: Double, - speed: Double? - ); - - abstract fun loadContent( - contentType: String, - content: String, - resumePosition: Double, - duration: Double, - speed: Double? - ); - - open fun changeVolume(volume: Double) { - throw NotImplementedError() - } - - open fun changeSpeed(speed: Double) { - throw NotImplementedError() - } - - abstract fun start(); - abstract fun stop(); - - abstract fun getDeviceInfo(): CastingDeviceInfo; - - abstract fun getAddresses(): List; -} - -class CastingDeviceLegacyWrapper(val inner: CastingDeviceLegacy) : CastingDevice() { - override val isReady: Boolean get() = inner.isReady - override val usedRemoteAddress: InetAddress? get() = inner.usedRemoteAddress - override val localAddress: InetAddress? get() = inner.localAddress - override val name: String? get() = inner.name - override val onConnectionStateChanged: Event1 get() = inner.onConnectionStateChanged - override val onPlayChanged: Event1 get() = inner.onPlayChanged - override val onTimeChanged: Event1 get() = inner.onTimeChanged - override val onDurationChanged: Event1 get() = inner.onDurationChanged - override val onVolumeChanged: Event1 get() = inner.onVolumeChanged - override val onSpeedChanged: Event1 get() = inner.onSpeedChanged - override val onMediaItemEnd: Event0 = Event0() - override var connectionState: CastConnectionState - get() = inner.connectionState - set(_) = Unit - override val protocolType: CastProtocolType get() = inner.protocol - override var isPlaying: Boolean - get() = inner.isPlaying - set(_) = Unit - override val expectedCurrentTime: Double - get() = inner.expectedCurrentTime - override var speed: Double - get() = inner.speed - set(_) = Unit - override var time: Double - get() = inner.time - set(_) = Unit - override var duration: Double - get() = inner.duration - set(_) = Unit - override var volume: Double - get() = inner.volume - set(_) = Unit - - override fun canSetVolume(): Boolean = inner.canSetVolume - override fun canSetSpeed(): Boolean = inner.canSetSpeed - override fun resumePlayback() = inner.resumeVideo() - override fun pausePlayback() = inner.pauseVideo() - override fun stopPlayback() = inner.stopVideo() - override fun seekTo(timeSeconds: Double) = inner.seekVideo(timeSeconds) - override fun changeVolume(timeSeconds: Double) = inner.changeVolume(timeSeconds) - override fun changeSpeed(speed: Double) = inner.changeSpeed(speed) - override fun connect() = inner.start() - override fun disconnect() = inner.stop() - override fun getDeviceInfo(): CastingDeviceInfo = inner.getDeviceInfo() - override fun getAddresses(): List = inner.getAddresses() - override fun loadVideo( - streamType: String, - contentType: String, - contentId: String, - resumePosition: Double, - duration: Double, - speed: Double?, - metadata: Metadata? - ) = inner.loadVideo(streamType, contentType, contentId, resumePosition, duration, speed) - - override fun loadContent( - contentType: String, - content: String, - resumePosition: Double, - duration: Double, - speed: Double?, - metadata: Metadata? - ) = inner.loadContent(contentType, content, resumePosition, duration, speed) - - override fun ensureThreadStarted() = when (inner) { - is FCastCastingDevice -> inner.ensureThreadStarted() - is ChromecastCastingDevice -> inner.ensureThreadsStarted() - else -> {} - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt deleted file mode 100644 index ed10832c..00000000 --- a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt +++ /dev/null @@ -1,736 +0,0 @@ -package com.futo.platformplayer.casting - -import android.os.Looper -import android.util.Log -import com.futo.platformplayer.getConnectedSocket -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.models.CastingDeviceInfo -import com.futo.platformplayer.protos.ChromeCast -import com.futo.platformplayer.toHexString -import com.futo.platformplayer.toInetAddress -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.json.JSONObject -import java.io.DataInputStream -import java.io.DataOutputStream -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.Socket -import java.security.cert.X509Certificate -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLSocket -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager - -class ChromecastCastingDevice : CastingDeviceLegacy { - //See for more info: https://developers.google.com/cast/docs/media/messages - - override val protocol: CastProtocolType get() = CastProtocolType.CHROMECAST; - override val isReady: Boolean get() = name != null && addresses != null && addresses?.isNotEmpty() == true && port != 0; - override var usedRemoteAddress: InetAddress? = null; - override var localAddress: InetAddress? = null; - override val canSetVolume: Boolean get() = true; - override val canSetSpeed: Boolean get() = true; - - var addresses: Array? = null; - var port: Int = 0; - - private var _streamType: String? = null; - private var _contentType: String? = null; - private var _contentId: String? = null; - - private var _socket: SSLSocket? = null; - private var _outputStream: DataOutputStream? = null; - private var _outputStreamLock = Object(); - private var _inputStream: DataInputStream? = null; - private var _inputStreamLock = Object(); - private var _scopeIO: CoroutineScope? = null; - private var _requestId = 1; - private var _started: Boolean = false; - private var _sessionId: String? = null; - private var _transportId: String? = null; - private var _launching = false; - private var _mediaSessionId: Int? = null; - private var _thread: 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 - private var _autoLaunchEnabled = true - - constructor(name: String, addresses: Array, port: Int) : super() { - this.name = name; - this.addresses = addresses; - this.port = port; - } - - constructor(deviceInfo: CastingDeviceInfo) : super() { - this.name = deviceInfo.name; - this.addresses = deviceInfo.addresses.map { a -> a.toInetAddress() }.filterNotNull().toTypedArray(); - this.port = deviceInfo.port; - } - - override fun getAddresses(): List { - return addresses?.toList() ?: listOf(); - } - - override fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double, speed: Double?) { - if (invokeInIOScopeIfRequired({ loadVideo(streamType, contentType, contentId, resumePosition, duration, speed) })) { - return; - } - - Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)"); - - setTime(resumePosition); - setDuration(duration); - _streamType = streamType; - _contentType = contentType; - _contentId = contentId; - - playVideo(); - } - - override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) { - //TODO: Can maybe be implemented by sending data:contentType,base64... - throw NotImplementedError(); - } - - private fun connectMediaChannel(transportId: String) { - val connectObject = JSONObject(); - connectObject.put("type", "CONNECT"); - connectObject.put("connType", 0); - sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.tp.connection", connectObject.toString()); - } - - private fun requestMediaStatus() { - val transportId = _transportId ?: return; - - val loadObject = JSONObject(); - loadObject.put("type", "GET_STATUS"); - loadObject.put("requestId", _requestId++); - sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", loadObject.toString()); - } - - private fun playVideo() { - val transportId = _transportId ?: return; - val contentId = _contentId ?: return; - val streamType = _streamType ?: return; - val contentType = _contentType ?: return; - - val loadObject = JSONObject(); - loadObject.put("type", "LOAD"); - - val mediaObject = JSONObject(); - mediaObject.put("contentId", contentId); - mediaObject.put("streamType", streamType); - mediaObject.put("contentType", contentType); - - if (time > 0.0) { - val seekTime = time; - loadObject.put("currentTime", seekTime); - } - - loadObject.put("media", mediaObject); - loadObject.put("requestId", _requestId++); - - - //TODO: This replace is necessary to get rid of backward slashes added by the JSON Object serializer - val json = loadObject.toString().replace("\\/","/"); - sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", json); - } - - override fun changeSpeed(speed: Double) { - if (invokeInIOScopeIfRequired { changeSpeed(speed) }) return - - val speedClamped = speed.coerceAtLeast(1.0).coerceAtLeast(1.0).coerceAtMost(2.0) - setSpeed(speedClamped) - val mediaSessionId = _mediaSessionId ?: return - val transportId = _transportId ?: return - val setSpeedObject = JSONObject().apply { - put("type", "SET_PLAYBACK_RATE") - put("mediaSessionId", mediaSessionId) - put("playbackRate", speedClamped) - put("requestId", _requestId++) - } - - sendChannelMessage(sourceId = "sender-0", destinationId = transportId, namespace = "urn:x-cast:com.google.cast.media", json = setSpeedObject.toString()) - } - - override fun changeVolume(volume: Double) { - if (invokeInIOScopeIfRequired({ changeVolume(volume) })) { - return; - } - - setVolume(volume) - val setVolumeObject = JSONObject(); - setVolumeObject.put("type", "SET_VOLUME"); - - val volumeObject = JSONObject(); - volumeObject.put("level", volume) - setVolumeObject.put("volume", volumeObject); - - setVolumeObject.put("requestId", _requestId++); - sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.receiver", setVolumeObject.toString()); - } - - override fun seekVideo(timeSeconds: Double) { - if (invokeInIOScopeIfRequired({ seekVideo(timeSeconds) })) { - return; - } - - val transportId = _transportId ?: return; - val mediaSessionId = _mediaSessionId ?: return; - - val loadObject = JSONObject(); - loadObject.put("type", "SEEK"); - loadObject.put("mediaSessionId", mediaSessionId); - loadObject.put("requestId", _requestId++); - loadObject.put("currentTime", timeSeconds); - sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", loadObject.toString()); - } - - override fun resumeVideo() { - if (invokeInIOScopeIfRequired(::resumeVideo)) { - return; - } - - val transportId = _transportId ?: return; - val mediaSessionId = _mediaSessionId ?: return; - - val loadObject = JSONObject(); - loadObject.put("type", "PLAY"); - loadObject.put("mediaSessionId", mediaSessionId); - loadObject.put("requestId", _requestId++); - sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", loadObject.toString()); - } - - override fun pauseVideo() { - if (invokeInIOScopeIfRequired(::pauseVideo)) { - return; - } - - val transportId = _transportId ?: return; - val mediaSessionId = _mediaSessionId ?: return; - - val loadObject = JSONObject(); - loadObject.put("type", "PAUSE"); - loadObject.put("mediaSessionId", mediaSessionId); - loadObject.put("requestId", _requestId++); - sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", loadObject.toString()); - } - - override fun stopVideo() { - if (invokeInIOScopeIfRequired(::stopVideo)) { - return; - } - - val transportId = _transportId ?: return; - val mediaSessionId = _mediaSessionId ?: return; - _contentId = null; - _contentType = null; - _streamType = null; - - val loadObject = JSONObject(); - loadObject.put("type", "STOP"); - loadObject.put("mediaSessionId", mediaSessionId); - loadObject.put("requestId", _requestId++); - sendChannelMessage("sender-0", transportId, "urn:x-cast:com.google.cast.media", loadObject.toString()); - } - - private fun launchPlayer() { - if (invokeInIOScopeIfRequired(::launchPlayer)) { - return; - } - - val launchObject = JSONObject(); - launchObject.put("type", "LAUNCH"); - launchObject.put("appId", "CC1AD845"); - launchObject.put("requestId", _requestId++); - sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.receiver", launchObject.toString()); - _lastLaunchTime_ms = System.currentTimeMillis() - } - - private fun getStatus() { - if (invokeInIOScopeIfRequired(::getStatus)) { - return; - } - - val launchObject = JSONObject(); - launchObject.put("type", "GET_STATUS"); - launchObject.put("requestId", _requestId++); - sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.receiver", launchObject.toString()); - } - - private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean { - if(Looper.getMainLooper().thread == Thread.currentThread()) { - _scopeIO?.launch { action(); } - return true; - } - - return false; - } - - override fun stopCasting() { - if (invokeInIOScopeIfRequired(::stopCasting)) { - return; - } - - val sessionId = _sessionId; - if (sessionId != null) { - val launchObject = JSONObject(); - launchObject.put("type", "STOP"); - launchObject.put("sessionId", sessionId); - launchObject.put("requestId", _requestId++); - sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.receiver", launchObject.toString()); - - _contentId = null; - _contentType = null; - _streamType = null; - _sessionId = null; - _launchRetries = 0 - _transportId = null; - } - - Logger.i(TAG, "Stopping active device because stopCasting was called.") - stop(); - } - - override fun start() { - if (_started) { - return; - } - - _autoLaunchEnabled = true - _started = true; - _sessionId = null; - _launchRetries = 0 - _mediaSessionId = null; - - Logger.i(TAG, "Starting..."); - - _launching = true; - - ensureThreadsStarted(); - Logger.i(TAG, "Started."); - } - - fun ensureThreadsStarted() { - val adrs = addresses ?: return; - - val thread = _thread - val pingThread = _pingThread - if (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive) { - Log.i(TAG, "Restarting threads because one of the threads has died") - - _scopeIO?.cancel(); - Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.") - _scopeIO = CoroutineScope(Dispatchers.IO); - - _thread = Thread { - connectionState = CastConnectionState.CONNECTING; - - var connectedSocket: Socket? = null - while (_scopeIO?.isActive == true) { - try { - val resultSocket = getConnectedSocket(adrs.toList(), port); - if (resultSocket == null) { - Thread.sleep(1000); - continue; - } - - connectedSocket = resultSocket - usedRemoteAddress = connectedSocket.inetAddress; - localAddress = connectedSocket.localAddress; - break; - } catch (e: Throwable) { - Logger.w(TAG, "Failed to get setup initial connection to ChromeCast device.", e) - Thread.sleep(1000); - } - } - - val sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustAllCerts, null); - - val factory = sslContext.socketFactory; - - val address = InetSocketAddress(usedRemoteAddress, port) - - //Connection loop - while (_scopeIO?.isActive == true) { - _sessionId = null; - _launchRetries = 0 - _mediaSessionId = null; - - Logger.i(TAG, "Connecting to Chromecast."); - connectionState = CastConnectionState.CONNECTING; - - try { - _socket?.close() - if (connectedSocket != null) { - Logger.i(TAG, "Using connected socket.") - _socket = factory.createSocket(connectedSocket, connectedSocket.inetAddress.hostAddress, connectedSocket.port, true) as SSLSocket - connectedSocket = null - } else { - Logger.i(TAG, "Using new socket.") - val s = Socket().apply { this.connect(address, 2000) } - _socket = factory.createSocket(s, s.inetAddress.hostAddress, s.port, true) as SSLSocket - } - - _socket?.startHandshake(); - Logger.i(TAG, "Successfully connected to Chromecast at $usedRemoteAddress:$port"); - - try { - _outputStream = DataOutputStream(_socket?.outputStream); - _inputStream = DataInputStream(_socket?.inputStream); - } catch (e: Throwable) { - Logger.i(TAG, "Failed to authenticate to Chromecast.", e); - } - } catch (e: Throwable) { - _socket?.close(); - Logger.i(TAG, "Failed to connect to Chromecast.", e); - - connectionState = CastConnectionState.CONNECTING; - Thread.sleep(1000); - continue; - } - - localAddress = _socket?.localAddress; - - try { - val connectObject = JSONObject(); - connectObject.put("type", "CONNECT"); - connectObject.put("connType", 0); - sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.connection", connectObject.toString()); - } catch (e: Throwable) { - Logger.i(TAG, "Failed to send connect message to Chromecast.", e); - _socket?.close(); - - connectionState = CastConnectionState.CONNECTING; - Thread.sleep(1000); - continue; - } - - getStatus(); - - val buffer = ByteArray(409600); - - Logger.i(TAG, "Started receiving."); - while (_scopeIO?.isActive == true) { - try { - val inputStream = _inputStream ?: break; - - val message = synchronized(_inputStreamLock) - { - Log.d(TAG, "Receiving next packet..."); - val b1 = inputStream.readUnsignedByte(); - val b2 = inputStream.readUnsignedByte(); - val b3 = inputStream.readUnsignedByte(); - val b4 = inputStream.readUnsignedByte(); - val size = - ((b1.toLong() shl 24) or (b2.toLong() shl 16) or (b3.toLong() shl 8) or b4.toLong()).toInt(); - if (size > buffer.size) { - Logger.w(TAG, "Skipping packet that is too large $size bytes.") - inputStream.skip(size.toLong()); - return@synchronized null - } - - Log.d(TAG, "Received header indicating $size bytes. Waiting for message."); - inputStream.read(buffer, 0, size); - - //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)); - Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}."); - val msg = ChromeCast.CastMessage.parseFrom(messageBytes); - if (msg.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") { - Logger.i(TAG, "Received message: $msg"); - } - return@synchronized msg - } - - if (message != null) { - try { - handleMessage(message); - } catch (e: Throwable) { - Logger.w(TAG, "Failed to handle message.", e); - break - } - } - } catch (e: java.net.SocketException) { - Logger.e(TAG, "Socket exception while receiving.", e); - break; - } catch (e: Throwable) { - Logger.e(TAG, "Exception while receiving.", e); - break; - } - } - _socket?.close(); - Logger.i(TAG, "Socket disconnected."); - - connectionState = CastConnectionState.CONNECTING; - Thread.sleep(1000); - } - - Logger.i(TAG, "Stopped connection loop."); - connectionState = CastConnectionState.DISCONNECTED; - }.apply { start() }; - - //Start ping loop - _pingThread = Thread { - Logger.i(TAG, "Started ping loop.") - - val pingObject = JSONObject(); - pingObject.put("type", "PING"); - - while (_scopeIO?.isActive == true) { - try { - sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString()); - } catch (e: Throwable) { - Log.w(TAG, "Failed to send ping."); - } - - Thread.sleep(5000); - } - - Logger.i(TAG, "Stopped ping loop."); - }.apply { start() }; - } else { - Log.i(TAG, "Threads still alive, not restarted") - } - } - - private fun sendChannelMessage(sourceId: String, destinationId: String, namespace: String, json: String) { - try { - val castMessage = ChromeCast.CastMessage.newBuilder() - .setProtocolVersion(ChromeCast.CastMessage.ProtocolVersion.CASTV2_1_0) - .setSourceId(sourceId) - .setDestinationId(destinationId) - .setNamespace(namespace) - .setPayloadType(ChromeCast.CastMessage.PayloadType.STRING) - .setPayloadUtf8(json) - .build(); - - sendMessage(castMessage.toByteArray()); - - if (namespace != "urn:x-cast:com.google.cast.tp.heartbeat") { - //Log.d(TAG, "Sent channel message: $castMessage"); - } - } catch (e: Throwable) { - Logger.w(TAG, "Failed to send channel message (sourceId: $sourceId, destinationId: $destinationId, namespace: $namespace, json: $json)", e); - _socket?.close(); - Logger.i(TAG, "Socket disconnected."); - - connectionState = CastConnectionState.CONNECTING; - } - } - - private fun handleMessage(message: ChromeCast.CastMessage) { - if (message.payloadType == ChromeCast.CastMessage.PayloadType.STRING) { - val jsonObject = JSONObject(message.payloadUtf8); - val type = jsonObject.getString("type"); - if (type == "RECEIVER_STATUS") { - val status = jsonObject.getJSONObject("status"); - - var sessionIsRunning = false; - if (status.has("applications")) { - val applications = status.getJSONArray("applications"); - - for (i in 0 until applications.length()) { - val applicationUpdate = applications.getJSONObject(i); - - val appId = applicationUpdate.getString("appId"); - Logger.i(TAG, "Status update received appId (appId: $appId)"); - - if (appId == "CC1AD845") { - sessionIsRunning = true; - _autoLaunchEnabled = false - - if (_sessionId == null) { - connectionState = CastConnectionState.CONNECTED; - _sessionId = applicationUpdate.getString("sessionId"); - _launchRetries = 0 - - val transportId = applicationUpdate.getString("transportId"); - connectMediaChannel(transportId); - Logger.i(TAG, "Connected to media channel $transportId"); - _transportId = transportId; - - requestMediaStatus(); - } - } - } - } - - if (!sessionIsRunning) { - if (System.currentTimeMillis() - _lastLaunchTime_ms > 5000) { - _sessionId = null - _mediaSessionId = null - _transportId = null - - if (_autoLaunchEnabled) { - if (_launching && _launchRetries < MAX_LAUNCH_RETRIES) { - Logger.i(TAG, "No player yet; attempting launch #${_launchRetries + 1}") - _launchRetries++ - launchPlayer() - } else { - // 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 { - Logger.e(TAG, "Player not found ($_launchRetries, _autoLaunchEnabled = $_autoLaunchEnabled); giving up.") - Logger.i(TAG, "Unable to start media receiver on device") - stop() - } - } else { - 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 - _autoLaunchEnabled = false - } - - val volume = status.getJSONObject("volume"); - //val volumeControlType = volume.getString("controlType"); - val volumeLevel = volume.getString("level").toDouble(); - val volumeMuted = volume.getBoolean("muted"); - //val volumeStepInterval = volume.getString("stepInterval").toFloat(); - setVolume(if (volumeMuted) 0.0 else volumeLevel); - - Logger.i(TAG, "Status update received volume (level: $volumeLevel, muted: $volumeMuted)"); - } else if (type == "MEDIA_STATUS") { - val statuses = jsonObject.getJSONArray("status"); - for (i in 0 until statuses.length()) { - val status = statuses.getJSONObject(i); - _mediaSessionId = status.getInt("mediaSessionId"); - - val playerState = status.getString("playerState"); - val currentTime = status.getDouble("currentTime"); - if (status.has("media")) { - val media = status.getJSONObject("media") - if (media.has("duration")) { - setDuration(media.getDouble("duration")) - } - } - - isPlaying = playerState == "PLAYING"; - if (isPlaying || playerState == "PAUSED") { - setTime(currentTime); - } - - val playbackRate = status.getInt("playbackRate"); - Logger.i(TAG, "Media update received (mediaSessionId: $_mediaSessionId, playedState: $playerState, currentTime: $currentTime, playbackRate: $playbackRate)"); - - if (_contentType == null) { - stopVideo(); - } - } - - val needsLoad = statuses.length() == 0 || (statuses.getJSONObject(0).getString("playerState") == "IDLE") - if (needsLoad && _contentId != null && _mediaSessionId == null) { - Logger.i(TAG, "Receiver idle, sending initial LOAD") - playVideo() - } - } else if (type == "CLOSE") { - if (message.sourceId == "receiver-0") { - Logger.i(TAG, "Close received."); - stopCasting(); - } else if (_transportId == message.sourceId) { - throw Exception("Transport id closed.") - } - } - } else { - throw Exception("Payload type ${message.payloadType} is not implemented."); - } - } - - private fun sendMessage(data: ByteArray) { - val outputStream = _outputStream; - if (outputStream == null) { - Logger.w(TAG, "Failed to send ${data.size} bytes, output stream is null."); - return; - } - - synchronized(_outputStreamLock) - { - val serializedSizeBE = ByteArray(4); - serializedSizeBE[0] = (data.size shr 24 and 0xff).toByte(); - serializedSizeBE[1] = (data.size shr 16 and 0xff).toByte(); - serializedSizeBE[2] = (data.size shr 8 and 0xff).toByte(); - serializedSizeBE[3] = (data.size and 0xff).toByte(); - outputStream.write(serializedSizeBE); - outputStream.write(data); - } - - //Log.d(TAG, "Sent ${data.size} bytes."); - } - - override fun stop() { - Logger.i(TAG, "Stopping..."); - usedRemoteAddress = null; - localAddress = null; - _started = false; - - _contentId = null - _contentType = null - _streamType = null - - _retryJob?.cancel() - _retryJob = null - - val socket = _socket; - val scopeIO = _scopeIO; - - if (scopeIO != null && socket != null) { - Logger.i(TAG, "Cancelling scopeIO with open socket.") - - scopeIO.launch { - socket.close(); - connectionState = CastConnectionState.DISCONNECTED; - scopeIO.cancel(); - Logger.i(TAG, "Cancelled scopeIO with open socket.") - } - } else { - scopeIO?.cancel(); - Logger.i(TAG, "Cancelled scopeIO without open socket.") - } - - _pingThread = null; - _thread = null; - _scopeIO = null; - _socket = null; - _outputStream = null; - _inputStream = null; - _mediaSessionId = null; - connectionState = CastConnectionState.DISCONNECTED; - } - - override fun getDeviceInfo(): CastingDeviceInfo { - return CastingDeviceInfo(name!!, CastProtocolType.CHROMECAST, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port); - } - - companion object { - val TAG = "ChromecastCastingDevice"; - - val trustAllCerts: Array = arrayOf(object : X509TrustManager { - override fun checkClientTrusted(chain: Array?, authType: String?) { } - override fun checkServerTrusted(chain: Array?, authType: String?) { } - override fun getAcceptedIssuers(): Array { return emptyArray(); } - }); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt deleted file mode 100644 index 71d1890b..00000000 --- a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt +++ /dev/null @@ -1,636 +0,0 @@ -package com.futo.platformplayer.casting - -import android.os.Looper -import android.util.Base64 -import android.util.Log -import com.futo.platformplayer.UIDialogs -import com.futo.platformplayer.casting.models.FCastDecryptedMessage -import com.futo.platformplayer.casting.models.FCastEncryptedMessage -import com.futo.platformplayer.casting.models.FCastKeyExchangeMessage -import com.futo.platformplayer.casting.models.FCastPlayMessage -import com.futo.platformplayer.casting.models.FCastPlaybackErrorMessage -import com.futo.platformplayer.casting.models.FCastPlaybackUpdateMessage -import com.futo.platformplayer.casting.models.FCastSeekMessage -import com.futo.platformplayer.casting.models.FCastSetSpeedMessage -import com.futo.platformplayer.casting.models.FCastSetVolumeMessage -import com.futo.platformplayer.casting.models.FCastVersionMessage -import com.futo.platformplayer.casting.models.FCastVolumeUpdateMessage -import com.futo.platformplayer.ensureNotMainThread -import com.futo.platformplayer.getConnectedSocket -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.models.CastingDeviceInfo -import com.futo.platformplayer.toHexString -import com.futo.platformplayer.toInetAddress -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.math.BigInteger -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.Socket -import java.security.KeyFactory -import java.security.KeyPair -import java.security.KeyPairGenerator -import java.security.MessageDigest -import java.security.PrivateKey -import java.security.spec.X509EncodedKeySpec -import javax.crypto.Cipher -import javax.crypto.KeyAgreement -import javax.crypto.spec.DHParameterSpec -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - -enum class Opcode(val value: Byte) { - None(0), - Play(1), - Pause(2), - Resume(3), - Stop(4), - Seek(5), - PlaybackUpdate(6), - VolumeUpdate(7), - SetVolume(8), - PlaybackError(9), - SetSpeed(10), - Version(11), - Ping(12), - Pong(13); - - companion object { - private val _map = entries.associateBy { it.value } - fun find(value: Byte): Opcode = _map[value] ?: Opcode.None - } -} - -class FCastCastingDevice : CastingDeviceLegacy { - //See for more info: TODO - - override val protocol: CastProtocolType get() = CastProtocolType.FCAST; - override val isReady: Boolean get() = name != null && addresses != null && addresses?.isNotEmpty() == true && port != 0; - override var usedRemoteAddress: InetAddress? = null; - override var localAddress: InetAddress? = null; - override val canSetVolume: Boolean get() = true; - override val canSetSpeed: Boolean get() = true; - - var addresses: Array? = null; - var port: Int = 0; - - private var _socket: Socket? = null; - private var _outputStream: OutputStream? = null; - private var _inputStream: InputStream? = null; - private var _scopeIO: CoroutineScope? = null; - private var _started: Boolean = false; - private var _version: Long = 1; - private var _thread: Thread? = null - private var _pingThread: Thread? = null - @Volatile private var _lastPongTime = System.currentTimeMillis() - private var _outputStreamLock = Object() - - constructor(name: String, addresses: Array, port: Int) : super() { - this.name = name; - this.addresses = addresses; - this.port = port; - } - - constructor(deviceInfo: CastingDeviceInfo) : super() { - this.name = deviceInfo.name; - this.addresses = deviceInfo.addresses.map { a -> a.toInetAddress() }.filterNotNull().toTypedArray(); - this.port = deviceInfo.port; - } - - override fun getAddresses(): List { - return addresses?.toList() ?: listOf(); - } - - override fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double, speed: Double?) { - if (invokeInIOScopeIfRequired({ loadVideo(streamType, contentType, contentId, resumePosition, duration, speed) })) { - return; - } - - //TODO: Remove this later, temporary for the transition - if (_version <= 1L) { - UIDialogs.toast("Version not received, if you are experiencing issues, try updating FCast") - } - - Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration, speed: $speed)"); - - setTime(resumePosition); - setDuration(duration); - send(Opcode.Play, FCastPlayMessage( - container = contentType, - url = contentId, - time = resumePosition, - speed = speed - )); - - setSpeed(speed ?: 1.0); - } - - override fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?) { - if (invokeInIOScopeIfRequired({ loadContent(contentType, content, resumePosition, duration, speed) })) { - return; - } - - //TODO: Remove this later, temporary for the transition - if (_version <= 1L) { - UIDialogs.toast("Version not received, if you are experiencing issues, try updating FCast") - } - - Logger.i(TAG, "Start streaming content (contentType: $contentType, resumePosition: $resumePosition, duration: $duration, speed: $speed)"); - - setTime(resumePosition); - setDuration(duration); - send(Opcode.Play, FCastPlayMessage( - container = contentType, - content = content, - time = resumePosition, - speed = speed - )); - - setSpeed(speed ?: 1.0); - } - - override fun changeVolume(volume: Double) { - if (invokeInIOScopeIfRequired({ changeVolume(volume) })) { - return; - } - - setVolume(volume); - send(Opcode.SetVolume, FCastSetVolumeMessage(volume)) - } - - override fun changeSpeed(speed: Double) { - if (invokeInIOScopeIfRequired({ changeSpeed(speed) })) { - return; - } - - setSpeed(speed); - send(Opcode.SetSpeed, FCastSetSpeedMessage(speed)) - } - - override fun seekVideo(timeSeconds: Double) { - if (invokeInIOScopeIfRequired({ seekVideo(timeSeconds) })) { - return; - } - - send(Opcode.Seek, FCastSeekMessage( - time = timeSeconds - )); - } - - override fun resumeVideo() { - if (invokeInIOScopeIfRequired(::resumeVideo)) { - return; - } - - send(Opcode.Resume); - } - - override fun pauseVideo() { - if (invokeInIOScopeIfRequired(::pauseVideo)) { - return; - } - - send(Opcode.Pause); - } - - override fun stopVideo() { - if (invokeInIOScopeIfRequired(::stopVideo)) { - return; - } - - send(Opcode.Stop); - } - - private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean { - if(Looper.getMainLooper().thread == Thread.currentThread()) { - _scopeIO?.launch { - try { - action(); - } catch (e: Throwable) { - Logger.e(TAG, "Failed to invoke in IO scope.", e) - } - } - return true; - } - - return false; - } - - override fun stopCasting() { - if (invokeInIOScopeIfRequired(::stopCasting)) { - return; - } - - stopVideo(); - - Logger.i(TAG, "Stopping active device because stopCasting was called.") - stop(); - } - - override fun start() { - if (_started) { - return; - } - - _started = true; - Logger.i(TAG, "Starting..."); - - ensureThreadStarted(); - Logger.i(TAG, "Started."); - } - - fun ensureThreadStarted() { - val adrs = addresses ?: return; - - val thread = _thread - val pingThread = _pingThread - if (_started && (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive)) { - Log.i(TAG, "(Re)starting thread because the thread has died") - - _scopeIO?.let { - it.cancel() - Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.") - } - - _scopeIO = CoroutineScope(Dispatchers.IO); - - _thread = Thread { - connectionState = CastConnectionState.CONNECTING; - Log.i(TAG, "Connection thread started.") - - var connectedSocket: Socket? = null - while (_scopeIO?.isActive == true) { - try { - Log.i(TAG, "getConnectedSocket (adrs = [ ${adrs.joinToString(", ")} ], port = ${port}).") - - val resultSocket = getConnectedSocket(adrs.toList(), port); - - if (resultSocket == null) { - Log.i(TAG, "Connection failed, waiting 1 seconds.") - Thread.sleep(1000); - continue; - } - - Log.i(TAG, "Connection succeeded.") - - connectedSocket = resultSocket - usedRemoteAddress = connectedSocket.inetAddress - localAddress = connectedSocket.localAddress - break; - } catch (e: Throwable) { - Logger.w(TAG, "Failed to get setup initial connection to FastCast device.", e) - Thread.sleep(1000); - } - } - - val address = InetSocketAddress(usedRemoteAddress, port) - - //Connection loop - while (_scopeIO?.isActive == true) { - Logger.i(TAG, "Connecting to FastCast."); - connectionState = CastConnectionState.CONNECTING; - - try { - _socket?.close() - _inputStream?.close() - _outputStream?.close() - if (connectedSocket != null) { - Logger.i(TAG, "Using connected socket."); - _socket = connectedSocket - connectedSocket = null - } else { - Logger.i(TAG, "Using new socket."); - _socket = Socket().apply { this.connect(address, 2000) }; - } - Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port"); - - _outputStream = _socket?.outputStream; - _inputStream = _socket?.inputStream; - } catch (e: IOException) { - _socket?.close() - _inputStream?.close() - _outputStream?.close() - Logger.i(TAG, "Failed to connect to FastCast.", e); - - connectionState = CastConnectionState.CONNECTING; - Thread.sleep(1000); - continue; - } - - localAddress = _socket?.localAddress - _lastPongTime = System.currentTimeMillis() - connectionState = CastConnectionState.CONNECTED - - val buffer = ByteArray(4096); - - Logger.i(TAG, "Started receiving."); - while (_scopeIO?.isActive == true) { - try { - val inputStream = _inputStream ?: break; - Log.d(TAG, "Receiving next packet..."); - - var headerBytesRead = 0 - while (headerBytesRead < 4) { - val read = inputStream.read(buffer, headerBytesRead, 4 - headerBytesRead) - if (read == -1) - throw Exception("Stream closed") - headerBytesRead += read - } - - val size = ((buffer[3].toUByte().toLong() shl 24) or (buffer[2].toUByte().toLong() shl 16) or (buffer[1].toUByte().toLong() shl 8) or buffer[0].toUByte().toLong()).toInt(); - if (size > buffer.size) { - Logger.w(TAG, "Packets larger than $size bytes are not supported.") - break - } - - Log.d(TAG, "Received header indicating $size bytes. Waiting for message."); - var bytesRead = 0 - while (bytesRead < size) { - val read = inputStream.read(buffer, bytesRead, size - bytesRead) - if (read == -1) - throw Exception("Stream closed") - bytesRead += read - } - - val messageBytes = buffer.sliceArray(IntRange(0, size)); - Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}."); - - val opcode = messageBytes[0]; - var json: String? = null; - if (size > 1) { - json = messageBytes.sliceArray(IntRange(1, size - 1)).decodeToString(); - } - - try { - handleMessage(Opcode.find(opcode), json); - } catch (e: Throwable) { - Logger.w(TAG, "Failed to handle message.", e) - break - } - } catch (e: java.net.SocketException) { - Logger.e(TAG, "Socket exception while receiving.", e); - break - } catch (e: Throwable) { - Logger.e(TAG, "Exception while receiving.", e); - break - } - } - - try { - _socket?.close() - _inputStream?.close() - _outputStream?.close() - Logger.i(TAG, "Socket disconnected."); - } catch (e: Throwable) { - Logger.e(TAG, "Failed to close socket.", e) - } - - connectionState = CastConnectionState.CONNECTING; - Thread.sleep(1000); - } - - Logger.i(TAG, "Stopped connection loop."); - connectionState = CastConnectionState.DISCONNECTED; - }.apply { start() } - - _pingThread = Thread { - Logger.i(TAG, "Started ping loop.") - while (_scopeIO?.isActive == true) { - if (connectionState == CastConnectionState.CONNECTED) { - try { - send(Opcode.Ping) - if (System.currentTimeMillis() - _lastPongTime > 15000) { - Logger.w(TAG, "Closing socket due to last pong time being larger than 15 seconds.") - try { - _socket?.close() - } catch (e: Throwable) { - Log.w(TAG, "Failed to close socket.", e) - } - } - } catch (e: Throwable) { - Log.w(TAG, "Failed to send ping.") - try { - _socket?.close() - _inputStream?.close() - _outputStream?.close() - } catch (e: Throwable) { - Log.w(TAG, "Failed to close socket.", e) - } - } - } - Thread.sleep(5000) - } - Logger.i(TAG, "Stopped ping loop.") - }.apply { start() } - } else { - Log.i(TAG, "Thread was still alive, not restarted") - } - } - - private fun handleMessage(opcode: Opcode, json: String? = null) { - Log.i(TAG, "Processing packet (opcode: $opcode, size: ${json?.length ?: 0})") - - when (opcode) { - Opcode.PlaybackUpdate -> { - if (json == null) { - Logger.w(TAG, "Got playback update without JSON, ignoring."); - return; - } - - val playbackUpdate = FCastCastingDevice.json.decodeFromString(json); - setTime(playbackUpdate.time, playbackUpdate.generationTime); - setDuration(playbackUpdate.duration, playbackUpdate.generationTime); - isPlaying = when (playbackUpdate.state) { - 1 -> true - else -> false - } - } - Opcode.VolumeUpdate -> { - if (json == null) { - Logger.w(TAG, "Got volume update without JSON, ignoring."); - return; - } - - val volumeUpdate = FCastCastingDevice.json.decodeFromString(json); - setVolume(volumeUpdate.volume, volumeUpdate.generationTime); - } - Opcode.PlaybackError -> { - if (json == null) { - Logger.w(TAG, "Got playback error without JSON, ignoring."); - return; - } - - val playbackError = FCastCastingDevice.json.decodeFromString(json); - Logger.e(TAG, "Remote casting playback error received: $playbackError") - } - Opcode.Version -> { - if (json == null) { - Logger.w(TAG, "Got version without JSON, ignoring."); - return; - } - - val version = FCastCastingDevice.json.decodeFromString(json); - _version = version.version; - Logger.i(TAG, "Remote version received: $version") - } - Opcode.Ping -> send(Opcode.Pong) - Opcode.Pong -> _lastPongTime = System.currentTimeMillis() - else -> { } - } - } - - private fun send(opcode: Opcode, message: String? = null) { - ensureNotMainThread() - - synchronized (_outputStreamLock) { - try { - val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0) - val size = 1 + data.size - val outputStream = _outputStream - if (outputStream == null) { - Log.w(TAG, "Failed to send $size bytes, output stream is null.") - return - } - - val serializedSizeLE = ByteArray(4) - serializedSizeLE[0] = (size and 0xff).toByte() - serializedSizeLE[1] = (size shr 8 and 0xff).toByte() - serializedSizeLE[2] = (size shr 16 and 0xff).toByte() - serializedSizeLE[3] = (size shr 24 and 0xff).toByte() - outputStream.write(serializedSizeLE) - - val opcodeBytes = ByteArray(1) - opcodeBytes[0] = opcode.value - outputStream.write(opcodeBytes) - - if (data.isNotEmpty()) { - outputStream.write(data) - } - - Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).") - } catch (e: Throwable) { - Log.i(TAG, "Failed to send message.", e) - throw e - } - } - } - - private inline fun send(opcode: Opcode, message: T) { - try { - send(opcode, message?.let { Json.encodeToString(it) }) - } catch (e: Throwable) { - Log.i(TAG, "Failed to encode message to string.", e) - throw e - } - } - - override fun stop() { - Logger.i(TAG, "Stopping..."); - usedRemoteAddress = null; - localAddress = null; - _started = false; - //TODO: Kill and/or join thread? - _thread = null; - _pingThread = null; - - val socket = _socket; - val scopeIO = _scopeIO; - - if (scopeIO != null && socket != null) { - Logger.i(TAG, "Cancelling scopeIO with open socket.") - - scopeIO.launch { - socket.close(); - _inputStream?.close() - _outputStream?.close() - connectionState = CastConnectionState.DISCONNECTED; - scopeIO.cancel(); - Logger.i(TAG, "Cancelled scopeIO with open socket.") - } - } else { - scopeIO?.cancel(); - Logger.i(TAG, "Cancelled scopeIO without open socket.") - } - - _scopeIO = null; - _socket = null; - _outputStream = null; - _inputStream = null; - connectionState = CastConnectionState.DISCONNECTED; - } - - override fun getDeviceInfo(): CastingDeviceInfo { - return CastingDeviceInfo(name!!, CastProtocolType.FCAST, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!! }.toTypedArray(), port); - } - - companion object { - val TAG = "FCastCastingDevice"; - private val json = Json { ignoreUnknownKeys = true } - - fun getKeyExchangeMessage(keyPair: KeyPair): FCastKeyExchangeMessage { - return FCastKeyExchangeMessage(1, Base64.encodeToString(keyPair.public.encoded, Base64.NO_WRAP)) - } - - fun generateKeyPair(): KeyPair { - //modp14 - val p = BigInteger("ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff", 16) - val g = BigInteger("2", 16) - val dhSpec = DHParameterSpec(p, g) - - val keyGen = KeyPairGenerator.getInstance("DH") - keyGen.initialize(dhSpec) - - return keyGen.generateKeyPair() - } - - fun computeSharedSecret(privateKey: PrivateKey, keyExchangeMessage: FCastKeyExchangeMessage): SecretKeySpec { - val keyFactory = KeyFactory.getInstance("DH") - val receivedPublicKeyBytes = Base64.decode(keyExchangeMessage.publicKey, Base64.NO_WRAP) - val receivedPublicKeySpec = X509EncodedKeySpec(receivedPublicKeyBytes) - val receivedPublicKey = keyFactory.generatePublic(receivedPublicKeySpec) - - val keyAgreement = KeyAgreement.getInstance("DH") - keyAgreement.init(privateKey) - keyAgreement.doPhase(receivedPublicKey, true) - - val sharedSecret = keyAgreement.generateSecret() - Log.i(TAG, "sharedSecret ${Base64.encodeToString(sharedSecret, Base64.NO_WRAP)}") - val sha256 = MessageDigest.getInstance("SHA-256") - val hashedSecret = sha256.digest(sharedSecret) - Log.i(TAG, "hashedSecret ${Base64.encodeToString(hashedSecret, Base64.NO_WRAP)}") - - return SecretKeySpec(hashedSecret, "AES") - } - - fun encryptMessage(aesKey: SecretKeySpec, decryptedMessage: FCastDecryptedMessage): FCastEncryptedMessage { - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.ENCRYPT_MODE, aesKey) - val iv = cipher.iv - val json = Json.encodeToString(decryptedMessage) - val encrypted = cipher.doFinal(json.toByteArray(Charsets.UTF_8)) - return FCastEncryptedMessage( - version = 1, - iv = Base64.encodeToString(iv, Base64.NO_WRAP), - blob = Base64.encodeToString(encrypted, Base64.NO_WRAP) - ) - } - - fun decryptMessage(aesKey: SecretKeySpec, encryptedMessage: FCastEncryptedMessage): FCastDecryptedMessage { - val iv = Base64.decode(encryptedMessage.iv, Base64.NO_WRAP) - val encrypted = Base64.decode(encryptedMessage.blob, Base64.NO_WRAP) - - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv)) - val decryptedJson = cipher.doFinal(encrypted) - return Json.decodeFromString(String(decryptedJson, Charsets.UTF_8)) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index 17ed068e..ddcb5d4a 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -8,6 +8,7 @@ import android.util.Log import androidx.annotation.OptIn import androidx.core.net.toUri import androidx.media3.common.util.UnstableApi +import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs @@ -57,16 +58,22 @@ import com.futo.platformplayer.views.casting.CastView.Companion import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.fcast.sender_sdk.CastContext +import org.fcast.sender_sdk.DeviceInfo import org.fcast.sender_sdk.Metadata +import org.fcast.sender_sdk.NsdDeviceDiscoverer +import org.fcast.sender_sdk.ProtocolType import java.net.Inet6Address import java.net.URLDecoder import java.net.URLEncoder import java.util.UUID import java.util.concurrent.atomic.AtomicInteger +import org.fcast.sender_sdk.DeviceInfo as RsDeviceInfo -abstract class StateCasting { +class StateCasting { val _scopeIO = CoroutineScope(Dispatchers.IO); val _scopeMain = CoroutineScope(Dispatchers.Main); private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get(); @@ -92,15 +99,163 @@ abstract class StateCasting { val isCasting: Boolean get() = activeDevice != null; private val _castId = AtomicInteger(0) - abstract fun handleUrl(url: String) - abstract fun onStop() - abstract fun start(context: Context) - abstract fun stop() + private val _context = CastContext() + var _deviceDiscoverer: NsdDeviceDiscoverer? = null - abstract fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice? - abstract fun startUpdateTimeJob( - onTimeJobTimeChanged_s: Event1, setTime: (Long) -> Unit - ): Job? + class DiscoveryEventHandler( + private val onDeviceAdded: (RsDeviceInfo) -> Unit, + private val onDeviceRemoved: (String) -> Unit, + private val onDeviceUpdated: (RsDeviceInfo) -> Unit, + ) : org.fcast.sender_sdk.DeviceDiscovererEventHandler { + override fun deviceAvailable(deviceInfo: RsDeviceInfo) { + onDeviceAdded(deviceInfo) + } + + override fun deviceChanged(deviceInfo: RsDeviceInfo) { + onDeviceUpdated(deviceInfo) + } + + override fun deviceRemoved(deviceName: String) { + onDeviceRemoved(deviceName) + } + } + + init { + if (BuildConfig.DEBUG) { + org.fcast.sender_sdk.initLogger(org.fcast.sender_sdk.LogLevelFilter.DEBUG) + } + } + + fun handleUrl(url: String) { + try { + val foundDeviceInfo = org.fcast.sender_sdk.deviceInfoFromUrl(url)!! + val foundDevice = _context.createDeviceFromInfo(foundDeviceInfo) + connectDevice(CastingDevice(foundDevice)) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to handle URL: $e") + } + } + + fun onStop() { + val ad = activeDevice ?: return + _resumeCastingDevice = ad.getDeviceInfo() + Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'") + Logger.i(TAG, "Stopping active device because of onStop.") + try { + ad.disconnect() + } catch (e: Throwable) { + Logger.e(TAG, "Failed to disconnect from device: $e") + } + } + + @Synchronized + fun start(context: Context) { + if (_started) + return + _started = true + + Log.i(TAG, "_resumeCastingDevice set null start") + _resumeCastingDevice = null + + Logger.i(TAG, "CastingService starting...") + + _castServer.start() + enableDeveloper(true) + + Logger.i(TAG, "CastingService started.") + + _deviceDiscoverer = NsdDeviceDiscoverer( + context, + DiscoveryEventHandler( + { deviceInfo -> // Added + Logger.i(TAG, "Device added: ${deviceInfo.name}") + val device = _context.createDeviceFromInfo(deviceInfo) + val deviceHandle = CastingDevice(device) + devices[deviceHandle.device.name()] = deviceHandle + invokeInMainScopeIfRequired { + onDeviceAdded.emit(deviceHandle) + } + }, + { deviceName -> // Removed + invokeInMainScopeIfRequired { + if (devices.containsKey(deviceName)) { + val device = devices.remove(deviceName) + if (device != null) { + onDeviceRemoved.emit(device) + } + } + } + }, + { deviceInfo -> // Updated + Logger.i(TAG, "Device updated: $deviceInfo") + val handle = devices[deviceInfo.name] + if (handle != null && handle is CastingDevice) { + handle.device.setPort(deviceInfo.port) + handle.device.setAddresses(deviceInfo.addresses) + invokeInMainScopeIfRequired { + onDeviceChanged.emit(handle) + } + } + }, + ) + ) + } + + @Synchronized + fun stop() { + if (!_started) { + return + } + + _started = false + + Logger.i(TAG, "CastingService stopping.") + + _scopeIO.cancel() + _scopeMain.cancel() + + Logger.i(TAG, "Stopping active device because StateCasting is being stopped.") + val d = activeDevice + activeDevice = null + try { + d?.disconnect() + } catch (e: Throwable) { + Logger.e(TAG, "Failed to disconnect device: $e") + } + + _castServer.stop() + _castServer.removeAllHandlers() + + Logger.i(TAG, "CastingService stopped.") + + _deviceDiscoverer = null + } + + fun startUpdateTimeJob( + onTimeJobTimeChanged_s: Event1, + setTime: (Long) -> Unit + ): Job? = null + + fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice? { + try { + val rsAddrs = + deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) } + val rsDeviceInfo = RsDeviceInfo( + name = deviceInfo.name, + protocol = when (deviceInfo.type) { + com.futo.platformplayer.casting.CastProtocolType.CHROMECAST -> ProtocolType.CHROMECAST + com.futo.platformplayer.casting.CastProtocolType.FCAST -> ProtocolType.F_CAST + else -> throw IllegalArgumentException() + }, + addresses = rsAddrs, + port = deviceInfo.port.toUShort(), + ) + + return CastingDevice(_context.createDeviceFromInfo(rsDeviceInfo)) + } catch (_: Throwable) { + return null + } + } fun onResume() { val ad = activeDevice @@ -1532,11 +1687,7 @@ abstract class StateCasting { } companion object { - var instance: StateCasting = if (Settings.instance.casting.experimentalCasting) { - StateCastingExp() - } else { - StateCastingLegacy() - } + var instance = StateCasting() private val representationRegex = Regex( "(.*?)<\\/Representation>", RegexOption.DOT_MATCHES_ALL diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCastingExp.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCastingExp.kt deleted file mode 100644 index 2f50ea88..00000000 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCastingExp.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.futo.platformplayer.casting - -import android.content.Context -import android.util.Log -import com.futo.platformplayer.BuildConfig -import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.models.CastingDeviceInfo -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import org.fcast.sender_sdk.DeviceInfo as RsDeviceInfo -import org.fcast.sender_sdk.ProtocolType -import org.fcast.sender_sdk.CastContext -import org.fcast.sender_sdk.NsdDeviceDiscoverer - -class StateCastingExp : StateCasting() { - private val _context = CastContext() - var _deviceDiscoverer: NsdDeviceDiscoverer? = null - - class DiscoveryEventHandler( - private val onDeviceAdded: (RsDeviceInfo) -> Unit, - private val onDeviceRemoved: (String) -> Unit, - private val onDeviceUpdated: (RsDeviceInfo) -> Unit, - ) : org.fcast.sender_sdk.DeviceDiscovererEventHandler { - override fun deviceAvailable(deviceInfo: RsDeviceInfo) { - onDeviceAdded(deviceInfo) - } - - override fun deviceChanged(deviceInfo: RsDeviceInfo) { - onDeviceUpdated(deviceInfo) - } - - override fun deviceRemoved(deviceName: String) { - onDeviceRemoved(deviceName) - } - } - - init { - if (BuildConfig.DEBUG) { - org.fcast.sender_sdk.initLogger(org.fcast.sender_sdk.LogLevelFilter.DEBUG) - } - } - - override fun handleUrl(url: String) { - try { - val foundDeviceInfo = org.fcast.sender_sdk.deviceInfoFromUrl(url)!! - val foundDevice = _context.createDeviceFromInfo(foundDeviceInfo) - connectDevice(CastingDeviceExp(foundDevice)) - } catch (e: Throwable) { - Logger.e(TAG, "Failed to handle URL: $e") - } - } - - override fun onStop() { - val ad = activeDevice ?: return - _resumeCastingDevice = ad.getDeviceInfo() - Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'") - Logger.i(TAG, "Stopping active device because of onStop.") - try { - ad.disconnect() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to disconnect from device: $e") - } - } - - @Synchronized - override fun start(context: Context) { - if (_started) - return - _started = true - - Log.i(TAG, "_resumeCastingDevice set null start") - _resumeCastingDevice = null - - Logger.i(TAG, "CastingService starting...") - - _castServer.start() - enableDeveloper(true) - - Logger.i(TAG, "CastingService started.") - - _deviceDiscoverer = NsdDeviceDiscoverer( - context, - DiscoveryEventHandler( - { deviceInfo -> // Added - Logger.i(TAG, "Device added: ${deviceInfo.name}") - val device = _context.createDeviceFromInfo(deviceInfo) - val deviceHandle = CastingDeviceExp(device) - devices[deviceHandle.device.name()] = deviceHandle - invokeInMainScopeIfRequired { - onDeviceAdded.emit(deviceHandle) - } - }, - { deviceName -> // Removed - invokeInMainScopeIfRequired { - if (devices.containsKey(deviceName)) { - val device = devices.remove(deviceName) - if (device != null) { - onDeviceRemoved.emit(device) - } - } - } - }, - { deviceInfo -> // Updated - Logger.i(TAG, "Device updated: $deviceInfo") - val handle = devices[deviceInfo.name] - if (handle != null && handle is CastingDeviceExp) { - handle.device.setPort(deviceInfo.port) - handle.device.setAddresses(deviceInfo.addresses) - invokeInMainScopeIfRequired { - onDeviceChanged.emit(handle) - } - } - }, - ) - ) - } - - @Synchronized - override fun stop() { - if (!_started) { - return - } - - _started = false - - Logger.i(TAG, "CastingService stopping.") - - _scopeIO.cancel() - _scopeMain.cancel() - - Logger.i(TAG, "Stopping active device because StateCasting is being stopped.") - val d = activeDevice - activeDevice = null - try { - d?.disconnect() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to disconnect device: $e") - } - - _castServer.stop() - _castServer.removeAllHandlers() - - Logger.i(TAG, "CastingService stopped.") - - _deviceDiscoverer = null - } - - override fun startUpdateTimeJob( - onTimeJobTimeChanged_s: Event1, - setTime: (Long) -> Unit - ): Job? = null - - override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDeviceExp? { - try { - val rsAddrs = - deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) } - val rsDeviceInfo = RsDeviceInfo( - name = deviceInfo.name, - protocol = when (deviceInfo.type) { - com.futo.platformplayer.casting.CastProtocolType.CHROMECAST -> ProtocolType.CHROMECAST - com.futo.platformplayer.casting.CastProtocolType.FCAST -> ProtocolType.F_CAST - else -> throw IllegalArgumentException() - }, - addresses = rsAddrs, - port = deviceInfo.port.toUShort(), - ) - - return CastingDeviceExp(_context.createDeviceFromInfo(rsDeviceInfo)) - } catch (_: Throwable) { - return null - } - } - - companion object { - private val TAG = "StateCastingExp" - } -} diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCastingLegacy.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCastingLegacy.kt deleted file mode 100644 index 02d84447..00000000 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCastingLegacy.kt +++ /dev/null @@ -1,399 +0,0 @@ -package com.futo.platformplayer.casting - -import android.content.Context -import android.net.Uri -import android.net.nsd.NsdManager -import android.net.nsd.NsdServiceInfo -import android.os.Build -import android.util.Base64 -import android.util.Log -import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.models.CastingDeviceInfo -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import java.net.InetAddress -import kotlinx.coroutines.delay - -class StateCastingLegacy : StateCasting() { - private var _nsdManager: NsdManager? = null - - private val _discoveryListeners = mapOf( - "_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice), - "_airplay._tcp" to createDiscoveryListener(::addOrUpdateAirPlayDevice), - "_fastcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice), - "_fcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice) - ) - - override fun handleUrl(url: String) { - val uri = Uri.parse(url) - if (uri.scheme != "fcast") { - throw Exception("Expected scheme to be FCast") - } - - val type = uri.host - if (type != "r") { - throw Exception("Expected type r") - } - - val connectionInfo = uri.pathSegments[0] - val json = - Base64.decode(connectionInfo, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) - .toString(Charsets.UTF_8) - val networkConfig = Json.decodeFromString(json) - val tcpService = networkConfig.services.first { v -> v.type == 0 } - - val foundInfo = addRememberedDevice( - CastingDeviceInfo( - name = networkConfig.name, - type = CastProtocolType.FCAST, - addresses = networkConfig.addresses.toTypedArray(), - port = tcpService.port - ) - ) - - if (foundInfo != null) { - connectDevice(deviceFromInfo(foundInfo)) - } - } - - override fun onStop() { - val ad = activeDevice ?: return; - _resumeCastingDevice = ad.getDeviceInfo() - Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'") - Logger.i(TAG, "Stopping active device because of onStop."); - ad.disconnect(); - } - - @Synchronized - override fun start(context: Context) { - if (_started) - return; - _started = true; - - Log.i(TAG, "_resumeCastingDevice set null start") - _resumeCastingDevice = null; - - Logger.i(TAG, "CastingService starting..."); - - _castServer.start(); - enableDeveloper(true); - - Logger.i(TAG, "CastingService started."); - - _nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager - startDiscovering() - } - - @Synchronized - private fun startDiscovering() { - _nsdManager?.apply { - _discoveryListeners.forEach { - discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value) - } - } - } - - @Synchronized - private fun stopDiscovering() { - _nsdManager?.apply { - _discoveryListeners.forEach { - try { - stopServiceDiscovery(it.value) - } catch (e: Throwable) { - Logger.w(TAG, "Failed to stop service discovery", e) - } - } - } - } - - @Synchronized - override fun stop() { - if (!_started) - return; - - _started = false; - - Logger.i(TAG, "CastingService stopping.") - - stopDiscovering() - _scopeIO.cancel(); - _scopeMain.cancel(); - - Logger.i(TAG, "Stopping active device because StateCasting is being stopped.") - val d = activeDevice; - activeDevice = null; - d?.disconnect(); - - _castServer.stop(); - _castServer.removeAllHandlers(); - - Logger.i(TAG, "CastingService stopped.") - - _nsdManager = null - } - - private fun createDiscoveryListener(addOrUpdate: (String, Array, Int) -> Unit): NsdManager.DiscoveryListener { - return object : NsdManager.DiscoveryListener { - override fun onDiscoveryStarted(regType: String) { - Log.d(TAG, "Service discovery started for $regType") - } - - override fun onDiscoveryStopped(serviceType: String) { - Log.i(TAG, "Discovery stopped: $serviceType") - } - - override fun onServiceLost(service: NsdServiceInfo) { - Log.e(TAG, "service lost: $service") - // TODO: Handle service lost, e.g., remove device - } - - override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { - Log.e(TAG, "Discovery failed for $serviceType: Error code:$errorCode") - try { - _nsdManager?.stopServiceDiscovery(this) - } catch (e: Throwable) { - Logger.w(TAG, "Failed to stop service discovery", e) - } - } - - override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { - Log.e(TAG, "Stop discovery failed for $serviceType: Error code:$errorCode") - try { - _nsdManager?.stopServiceDiscovery(this) - } catch (e: Throwable) { - Logger.w(TAG, "Failed to stop service discovery", e) - } - } - - override fun onServiceFound(service: NsdServiceInfo) { - Log.v(TAG, "Service discovery success for ${service.serviceType}: $service") - val addresses = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - service.hostAddresses.toTypedArray() - } else { - arrayOf(service.host) - } - addOrUpdate(service.serviceName, addresses, service.port) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - _nsdManager?.registerServiceInfoCallback( - service, - { it.run() }, - object : NsdManager.ServiceInfoCallback { - override fun onServiceUpdated(serviceInfo: NsdServiceInfo) { - Log.v(TAG, "onServiceUpdated: $serviceInfo") - addOrUpdate( - serviceInfo.serviceName, - serviceInfo.hostAddresses.toTypedArray(), - serviceInfo.port - ) - } - - override fun onServiceLost() { - Log.v(TAG, "onServiceLost: $service") - // TODO: Handle service lost - } - - override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) { - Log.v(TAG, "onServiceInfoCallbackRegistrationFailed: $errorCode") - } - - override fun onServiceInfoCallbackUnregistered() { - Log.v(TAG, "onServiceInfoCallbackUnregistered") - } - }) - } else { - _nsdManager?.resolveService(service, object : NsdManager.ResolveListener { - override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { - Log.v(TAG, "Resolve failed: $errorCode") - } - - override fun onServiceResolved(serviceInfo: NsdServiceInfo) { - Log.v(TAG, "Resolve Succeeded: $serviceInfo") - addOrUpdate( - serviceInfo.serviceName, - arrayOf(serviceInfo.host), - serviceInfo.port - ) - } - }) - } - } - } - } - - override fun startUpdateTimeJob( - onTimeJobTimeChanged_s: Event1, - setTime: (Long) -> Unit - ): Job? { - val d = activeDevice; - if (d is CastingDeviceLegacyWrapper && (d.inner is AirPlayCastingDevice || d.inner is ChromecastCastingDevice)) { - return _scopeMain.launch { - while (true) { - val device = instance.activeDevice - if (device == null || !device.isPlaying) { - break - } - - delay(1000) - val time_ms = (device.expectedCurrentTime * 1000.0).toLong() - setTime(time_ms) - onTimeJobTimeChanged_s.emit(device.expectedCurrentTime.toLong()) - } - } - } - return null - } - - override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice { - return CastingDeviceLegacyWrapper( - when (deviceInfo.type) { - CastProtocolType.CHROMECAST -> { - ChromecastCastingDevice(deviceInfo); - } - - CastProtocolType.AIRPLAY -> { - AirPlayCastingDevice(deviceInfo); - } - - CastProtocolType.FCAST -> { - FCastCastingDevice(deviceInfo); - } - } - ) - } - - private fun addOrUpdateChromeCastDevice( - name: String, - addresses: Array, - port: Int - ) { - return addOrUpdateCastDevice( - name, - deviceFactory = { - CastingDeviceLegacyWrapper( - ChromecastCastingDevice( - name, - addresses, - port - ) - ) - }, - deviceUpdater = { d -> - if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is ChromecastCastingDevice) { - return@addOrUpdateCastDevice false; - } - - val changed = - addresses.contentEquals(d.inner.addresses) || d.name != name || d.inner.port != port; - if (changed) { - d.inner.name = name; - d.inner.addresses = addresses; - d.inner.port = port; - } - - return@addOrUpdateCastDevice changed; - } - ); - } - - private fun addOrUpdateAirPlayDevice(name: String, addresses: Array, port: Int) { - return addOrUpdateCastDevice( - name, - deviceFactory = { - CastingDeviceLegacyWrapper( - AirPlayCastingDevice( - name, - addresses, - port - ) - ) - }, - deviceUpdater = { d -> - if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is AirPlayCastingDevice) { - return@addOrUpdateCastDevice false; - } - - val changed = - addresses.contentEquals(addresses) || d.name != name || d.inner.port != port; - if (changed) { - d.inner.name = name; - d.inner.port = port; - d.inner.addresses = addresses; - } - - return@addOrUpdateCastDevice changed; - } - ); - } - - private fun addOrUpdateFastCastDevice(name: String, addresses: Array, port: Int) { - return addOrUpdateCastDevice( - name, - deviceFactory = { CastingDeviceLegacyWrapper(FCastCastingDevice(name, addresses, port)) }, - deviceUpdater = { d -> - if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is FCastCastingDevice) { - return@addOrUpdateCastDevice false; - } - - val changed = - addresses.contentEquals(addresses) || d.name != name || d.inner.port != port; - if (changed) { - d.inner.name = name; - d.inner.port = port; - d.inner.addresses = addresses; - } - - return@addOrUpdateCastDevice changed; - } - ); - } - - private inline fun addOrUpdateCastDevice( - name: String, - deviceFactory: () -> CastingDevice, - deviceUpdater: (device: CastingDevice) -> Boolean - ) { - var invokeEvents: (() -> Unit)? = null; - - synchronized(devices) { - val device = devices[name]; - if (device != null) { - val changed = deviceUpdater(device); - if (changed) { - invokeEvents = { - onDeviceChanged.emit(device); - } - } - } else { - val newDevice = deviceFactory(); - this.devices[name] = newDevice - - invokeEvents = { - onDeviceAdded.emit(newDevice); - }; - } - } - - invokeEvents?.let { _scopeMain.launch { it(); }; }; - } - - @Serializable - private data class FCastNetworkConfig( - val name: String, - val addresses: List, - val services: List - ) - - @Serializable - private data class FCastService( - val port: Int, - val type: Int - ) - - companion object { - private val TAG = "StateCastingLegacy" - } -} diff --git a/app/src/main/java/com/futo/platformplayer/casting/models/FCast.kt b/app/src/main/java/com/futo/platformplayer/casting/models/FCast.kt deleted file mode 100644 index 79cc9579..00000000 --- a/app/src/main/java/com/futo/platformplayer/casting/models/FCast.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.futo.platformplayer.casting.models - -import kotlinx.serialization.Serializable - -@Serializable -data class FCastPlayMessage( - val container: String, - val url: String? = null, - val content: String? = null, - val time: Double? = null, - val speed: Double? = null -) { } - -@Serializable -data class FCastSeekMessage( - val time: Double -) { } - -@Serializable -data class FCastPlaybackUpdateMessage( - val generationTime: Long, - val time: Double, - val duration: Double, - val state: Int, - val speed: Double -) { } - - -@Serializable -data class FCastVolumeUpdateMessage( - val generationTime: Long, - val volume: Double -) - -@Serializable -data class FCastSetVolumeMessage( - val volume: Double -) - -@Serializable -data class FCastSetSpeedMessage( - val speed: Double -) - -@Serializable -data class FCastPlaybackErrorMessage( - val message: String -) - -@Serializable -data class FCastVersionMessage( - val version: Long -) - -@Serializable -data class FCastKeyExchangeMessage( - val version: Long, - val publicKey: String -) - -@Serializable -data class FCastDecryptedMessage( - val opcode: Long, - val message: String? -) - -@Serializable -data class FCastEncryptedMessage( - val version: Long, - val iv: String?, - val blob: String -) \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/CastingAddDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/CastingAddDialog.kt index ea8029ce..a4bbffc8 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/CastingAddDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/CastingAddDialog.kt @@ -40,13 +40,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) { _buttonConfirm = findViewById(R.id.button_confirm); _buttonTutorial = findViewById(R.id.button_tutorial) - val deviceTypeArray = if (Settings.instance.casting.experimentalCasting) { - R.array.exp_casting_device_type_array - } else { - R.array.casting_device_type_array - } - - ArrayAdapter.createFromResource(context, deviceTypeArray, R.layout.spinner_item_simple).also { adapter -> + ArrayAdapter.createFromResource(context, R.array.casting_device_type_array, R.layout.spinner_item_simple).also { adapter -> adapter.setDropDownViewResource(R.layout.spinner_dropdownitem_simple); _spinnerType.adapter = adapter; }; diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt index f8c75e89..bca820ea 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt @@ -12,7 +12,6 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import com.futo.platformplayer.R -import com.futo.platformplayer.Settings import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.casting.CastConnectionState import com.futo.platformplayer.casting.CastProtocolType @@ -174,13 +173,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) { _textType.text = "AirPlay"; } CastProtocolType.FCAST -> { - _imageDevice.setImageResource( - if (Settings.instance.casting.experimentalCasting) { - R.drawable.ic_exp_fc - } else { - R.drawable.ic_fc - } - ) + _imageDevice.setImageResource(R.drawable.ic_fc) _textType.text = "FCast"; } } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt index 32fb5367..be696a84 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt @@ -9,7 +9,6 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.futo.platformplayer.R -import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.casting.CastConnectionState import com.futo.platformplayer.casting.CastProtocolType @@ -91,13 +90,7 @@ class DeviceViewHolder : ViewHolder { _textType.text = "AirPlay"; } CastProtocolType.FCAST -> { - _imageDevice.setImageResource( - if (Settings.instance.casting.experimentalCasting) { - R.drawable.ic_exp_fc - } else { - R.drawable.ic_fc - } - ) + _imageDevice.setImageResource(R.drawable.ic_fc) _textType.text = "FCast"; } } diff --git a/app/src/main/proto/com/futo/platformplayer/protos/ChromeCast.proto b/app/src/main/proto/com/futo/platformplayer/protos/ChromeCast.proto deleted file mode 100644 index 395c4889..00000000 --- a/app/src/main/proto/com/futo/platformplayer/protos/ChromeCast.proto +++ /dev/null @@ -1,18 +0,0 @@ -syntax = "proto2"; -option optimize_for = LITE_RUNTIME; -package com.futo.platformplayer.protos; - -message CastMessage { - enum ProtocolVersion { CASTV2_1_0 = 0; } - required ProtocolVersion protocol_version = 1; - required string source_id = 2; - required string destination_id = 3; - required string namespace = 4; - enum PayloadType { - STRING = 0; - BINARY = 1; - } - required PayloadType payload_type = 5; - optional string payload_utf8 = 6; - optional bytes payload_binary = 7; -} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_exp_fc.xml b/app/src/main/res/drawable/ic_exp_fc.xml deleted file mode 100644 index 355f8836..00000000 --- a/app/src/main/res/drawable/ic_exp_fc.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_fc.xml b/app/src/main/res/drawable/ic_fc.xml index bbc5e8fa..355f8836 100644 --- a/app/src/main/res/drawable/ic_fc.xml +++ b/app/src/main/res/drawable/ic_fc.xml @@ -1,9 +1,14 @@ + android:width="111.96dp" + android:height="114.46dp" + android:viewportWidth="111.96" + android:viewportHeight="114.46"> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fb1e7b8..b4e6f199 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1112,11 +1112,6 @@ FCast ChromeCast - AirPlay - - - FCast - ChromeCast None