From e2a5665516792fbda07747e00370d84bffa671d0 Mon Sep 17 00:00:00 2001 From: Marcus Hanestad Date: Wed, 3 Sep 2025 18:06:17 +0200 Subject: [PATCH] casting: refactor SDK integration --- .../java/com/futo/platformplayer/UIDialogs.kt | 70 +- .../platformplayer/activities/MainActivity.kt | 14 +- .../casting/AirPlayCastingDevice.kt | 2 +- .../platformplayer/casting/CastingDevice.kt | 189 +- .../casting/ChomecastCastingDevice.kt | 2 +- .../casting/ExpCastingDevice.kt | 284 +++ .../platformplayer/casting/ExpStateCasting.kt | 174 ++ .../casting/FCastCastingDevice.kt | 5 +- .../casting/OldCastingDevice.kt | 243 +++ .../platformplayer/casting/OldStateCasting.kt | 397 ++++ .../platformplayer/casting/StateCasting.kt | 1676 ++++++++------- .../dialogs/CastingAddDialog.kt | 12 +- .../dialogs/ConnectCastingDialog.kt | 228 +-- .../dialogs/ConnectedCastingDialog.kt | 256 +-- .../experimental_casting/CastingDevice.kt | 181 -- .../experimental_casting/StateCasting.kt | 1813 ----------------- .../StateCastingDispatcher.kt | 132 -- .../mainactivity/main/VideoDetailFragment.kt | 2 +- .../mainactivity/main/VideoDetailView.kt | 199 +- .../models/CastingDeviceInfo.kt | 35 +- .../futo/platformplayer/states/StateApp.kt | 8 +- .../views/adapters/DeviceAdapter.kt | 26 +- .../views/adapters/DeviceViewHolder.kt | 227 +-- .../views/casting/CastButton.kt | 64 +- .../platformplayer/views/casting/CastView.kt | 106 +- 25 files changed, 2508 insertions(+), 3837 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/casting/ExpCastingDevice.kt create mode 100644 app/src/main/java/com/futo/platformplayer/casting/ExpStateCasting.kt create mode 100644 app/src/main/java/com/futo/platformplayer/casting/OldCastingDevice.kt create mode 100644 app/src/main/java/com/futo/platformplayer/casting/OldStateCasting.kt delete mode 100644 app/src/main/java/com/futo/platformplayer/experimental_casting/CastingDevice.kt delete mode 100644 app/src/main/java/com/futo/platformplayer/experimental_casting/StateCasting.kt delete mode 100644 app/src/main/java/com/futo/platformplayer/experimental_casting/StateCastingDispatcher.kt diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt index babac1ef..e88ff6a5 100644 --- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt +++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt @@ -7,7 +7,6 @@ import android.content.Intent import android.graphics.Color import android.graphics.drawable.Animatable import android.net.Uri -import android.text.Layout import android.text.method.ScrollingMovementMethod import android.util.TypedValue import android.view.Gravity @@ -22,6 +21,7 @@ import androidx.core.content.ContextCompat import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.casting.OldStateCasting import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.dialogs.AutoUpdateDialog import com.futo.platformplayer.dialogs.AutomaticBackupDialog @@ -38,7 +38,6 @@ import com.futo.platformplayer.dialogs.MigrateDialog import com.futo.platformplayer.dialogs.PluginUpdateDialog import com.futo.platformplayer.dialogs.ProgressDialog import com.futo.platformplayer.engine.exceptions.PluginException -import com.futo.platformplayer.experimental_casting.ExpStateCasting import com.futo.platformplayer.fragment.mainactivity.main.MainFragment import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment @@ -438,56 +437,29 @@ class UIDialogs { fun showCastingDialog(context: Context, ownerActivity: Activity? = null) { - if (Settings.instance.casting.experimentalCasting) { - val d = ExpStateCasting.instance.activeDevice; - if (d != null) { - val dialog = ConnectedCastingDialog(context); - if (context is Activity) { - dialog.setOwnerActivity(context) - } - registerDialogOpened(dialog); - ownerActivity?.let { dialog.setOwnerActivity(it) } - dialog.setOnDismissListener { registerDialogClosed(dialog) }; - dialog.show(); - } else { - val dialog = ConnectCastingDialog(context); - if (context is Activity) { - dialog.setOwnerActivity(context) - } - registerDialogOpened(dialog); - val c = context - if (c is Activity) { - dialog.setOwnerActivity(c); - } - ownerActivity?.let { dialog.setOwnerActivity(it) } - dialog.setOnDismissListener { registerDialogClosed(dialog) }; - dialog.show(); + val d = StateCasting.instance.activeDevice + if (d != null) { + val dialog = ConnectedCastingDialog(context); + if (context is Activity) { + dialog.setOwnerActivity(context) } + registerDialogOpened(dialog); + ownerActivity?.let { dialog.setOwnerActivity(it) } + dialog.setOnDismissListener { registerDialogClosed(dialog) }; + dialog.show(); } else { - val d = StateCasting.instance.activeDevice; - if (d != null) { - val dialog = ConnectedCastingDialog(context); - if (context is Activity) { - dialog.setOwnerActivity(context) - } - registerDialogOpened(dialog); - ownerActivity?.let { dialog.setOwnerActivity(it) } - dialog.setOnDismissListener { registerDialogClosed(dialog) }; - dialog.show(); - } else { - val dialog = ConnectCastingDialog(context); - if (context is Activity) { - dialog.setOwnerActivity(context) - } - registerDialogOpened(dialog); - val c = context - if (c is Activity) { - dialog.setOwnerActivity(c); - } - ownerActivity?.let { dialog.setOwnerActivity(it) } - dialog.setOnDismissListener { registerDialogClosed(dialog) }; - dialog.show(); + val dialog = ConnectCastingDialog(context); + if (context is Activity) { + dialog.setOwnerActivity(context) } + registerDialogOpened(dialog); + val c = context + if (c is Activity) { + dialog.setOwnerActivity(c); + } + ownerActivity?.let { dialog.setOwnerActivity(it) } + dialog.setOnDismissListener { registerDialogClosed(dialog) }; + dialog.show(); } } diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index d664ae9f..9993d4c3 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -42,7 +42,6 @@ import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.dp -import com.futo.platformplayer.experimental_casting.ExpStateCasting import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment @@ -118,7 +117,6 @@ import java.util.LinkedList import java.util.UUID import java.util.concurrent.ConcurrentLinkedQueue - class MainActivity : AppCompatActivity, IWithResultLauncher { //TODO: Move to dimensions @@ -508,11 +506,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { handleIntent(intent); if (Settings.instance.casting.enabled) { - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.start(this) - } else { - StateCasting.instance.start(this) - } + StateCasting.instance.start(this) } StatePlatform.instance.onDevSourceChanged.subscribe { @@ -1051,11 +1045,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { Logger.i(TAG, "handleFCast"); try { - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.handleUrl(this, url) - } else { - StateCasting.instance.handleUrl(this, url) - } + StateCasting.instance.handleUrl(url) return true; } catch (e: Throwable) { Log.e(TAG, "Failed to parse FCast URL '${url}'.", e) diff --git a/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt index 0cc1bebc..b89071f8 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.launch import java.net.InetAddress import java.util.UUID -class AirPlayCastingDevice : CastingDevice { +class AirPlayCastingDevice : OldCastingDevice { //See for more info: https://nto.github.io/AirPlay override val protocol: CastProtocolType get() = CastProtocolType.AIRPLAY; 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 69f74747..cb6c9ab0 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt @@ -2,147 +2,78 @@ package com.futo.platformplayer.casting 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 CastingDevice { - abstract val protocol: CastProtocolType; - abstract val isReady: Boolean; - abstract var usedRemoteAddress: InetAddress?; - abstract var localAddress: InetAddress?; - abstract val canSetVolume: Boolean; - abstract val canSetSpeed: Boolean; + 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 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 - var name: String? = null; - var isPlaying: Boolean = false - set(value) { - val changed = value != field; - field = value; - if (changed) { - onPlayChanged.emit(value); - } - }; + @Throws + abstract fun resumePlayback() - private var lastTimeChangeTime_ms: Long = 0 - var time: Double = 0.0 - private set + @Throws + abstract fun pausePlayback() - 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) - } - } + @Throws + abstract fun stopPlayback() - private var lastDurationChangeTime_ms: Long = 0 - var duration: Double = 0.0 - private set + @Throws + abstract fun seekTo(timeSeconds: Double) - 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) - } - } + @Throws + abstract fun changeVolume(timeSeconds: Double) - private var lastVolumeChangeTime_ms: Long = 0 - var volume: Double = 1.0 - private set + @Throws + abstract fun changeSpeed(speed: Double) - 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) - } - } + @Throws + abstract fun connect() - private var lastSpeedChangeTime_ms: Long = 0 - var speed: Double = 1.0 - private set + @Throws + abstract fun disconnect() + abstract fun getDeviceInfo(): CastingDeviceInfo + abstract fun getAddresses(): List - 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) - } - } + @Throws + abstract fun loadVideo( + streamType: String, + contentType: String, + contentId: String, + resumePosition: Double, + duration: Double, + speed: Double?, + metadata: Metadata? + ) - 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; + @Throws + abstract fun loadContent( + contentType: String, + content: String, + resumePosition: Double, + duration: Double, + speed: Double?, + metadata: Metadata? + ) - if (changed) { - onConnectionStateChanged.emit(value); - } - }; + abstract fun ensureThreadStarted() +} - 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; -} \ 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 index f6582055..d85a7607 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt @@ -27,7 +27,7 @@ import javax.net.ssl.SSLSocket import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager -class ChromecastCastingDevice : CastingDevice { +class ChromecastCastingDevice : OldCastingDevice { //See for more info: https://developers.google.com/cast/docs/media/messages override val protocol: CastProtocolType get() = CastProtocolType.CHROMECAST; diff --git a/app/src/main/java/com/futo/platformplayer/casting/ExpCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/ExpCastingDevice.kt new file mode 100644 index 00000000..dd094a4d --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/casting/ExpCastingDevice.kt @@ -0,0 +1,284 @@ +package com.futo.platformplayer.casting + +import android.os.Build +import com.futo.platformplayer.BuildConfig +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.models.CastingDeviceInfo +import org.fcast.sender_sdk.ApplicationInfo +import org.fcast.sender_sdk.GenericKeyEvent +import org.fcast.sender_sdk.GenericMediaEvent +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.IpAddr +import org.fcast.sender_sdk.LoadRequest +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 ExpCastingDevice(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() + + 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: GenericKeyEvent) { + // Unreachable + } + + override fun mediaEvent(event: GenericMediaEvent) { + // Unreachable + } + + 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 fun resumePlayback() = try { + device.resumePlayback() + } catch (_: Throwable) { + } + + override fun pausePlayback() = try { + device.pausePlayback() + } catch (_: Throwable) { + } + + override fun stopPlayback() = try { + device.stopPlayback() + } catch (_: Throwable) { + } + + override fun seekTo(timeSeconds: Double) = try { + device.seek(timeSeconds) + } catch (_: Throwable) { + } + + override fun changeVolume(timeSeconds: Double) = device.changeVolume(timeSeconds) + + 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? + ) = try { + device.load( + LoadRequest.Video( + contentType = contentType, + url = contentId, + resumePosition = resumePosition, + speed = speed, + volume = volume, + metadata = metadata + ) + ) + } catch (_: Throwable) { + } + + override fun loadContent( + contentType: String, + content: String, + resumePosition: Double, + duration: Double, + speed: Double?, + metadata: Metadata? + ) = try { + device.load( + LoadRequest.Content( + contentType = contentType, + content = content, + resumePosition = resumePosition, + speed = speed, + volume = volume, + metadata = metadata, + ) + ) + } catch (_: Throwable) { + } + + 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 -> { + 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.CONNECTING + 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") + } + } + } + } + + override fun ensureThreadStarted() {} + + companion object { + private val TAG = "ExperimentalCastingDevice" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/casting/ExpStateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/ExpStateCasting.kt new file mode 100644 index 00000000..fd4c75ae --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/casting/ExpStateCasting.kt @@ -0,0 +1,174 @@ +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 ExpStateCasting : 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(ExpCastingDevice(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 = ExpCastingDevice(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 ExpCastingDevice) { + 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): ExpCastingDevice { + val rsAddrs = + deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) } // Throws! + 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 ExpCastingDevice(_context.createDeviceFromInfo(rsDeviceInfo)) + } + + companion object { + private val TAG = "ExperimentalStateCasting" + } +} diff --git a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt index be62f726..27edb1db 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt @@ -3,7 +3,6 @@ package com.futo.platformplayer.casting import android.os.Looper import android.util.Base64 import android.util.Log -import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.casting.models.FCastDecryptedMessage import com.futo.platformplayer.casting.models.FCastEncryptedMessage @@ -25,7 +24,6 @@ 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 kotlinx.serialization.encodeToString @@ -34,7 +32,6 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.math.BigInteger -import java.net.Inet4Address import java.net.InetAddress import java.net.InetSocketAddress import java.net.Socket @@ -72,7 +69,7 @@ enum class Opcode(val value: Byte) { } } -class FCastCastingDevice : CastingDevice { +class FCastCastingDevice : OldCastingDevice { //See for more info: TODO override val protocol: CastProtocolType get() = CastProtocolType.FCAST; diff --git a/app/src/main/java/com/futo/platformplayer/casting/OldCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/OldCastingDevice.kt new file mode 100644 index 00000000..be3ac724 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/casting/OldCastingDevice.kt @@ -0,0 +1,243 @@ +package com.futo.platformplayer.casting + +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.DeviceConnectionState +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 OldCastingDevice { + 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 OldCastingDeviceWrapper(val inner: OldCastingDevice) : 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 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/OldStateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/OldStateCasting.kt new file mode 100644 index 00000000..ec0339ee --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/casting/OldStateCasting.kt @@ -0,0 +1,397 @@ +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 OldStateCasting : 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 + ) + ) + + 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 OldCastingDeviceWrapper && (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 OldCastingDeviceWrapper( + 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 = { + OldCastingDeviceWrapper( + ChromecastCastingDevice( + name, + addresses, + port + ) + ) + }, + deviceUpdater = { d -> + if (d.isReady || d !is OldCastingDeviceWrapper || 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 = { + OldCastingDeviceWrapper( + AirPlayCastingDevice( + name, + addresses, + port + ) + ) + }, + deviceUpdater = { d -> + if (d.isReady || d !is OldCastingDeviceWrapper || 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 = { OldCastingDeviceWrapper(FCastCastingDevice(name, addresses, port)) }, + deviceUpdater = { d -> + if (d.isReady || d !is OldCastingDeviceWrapper || 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 = "OldStateCasting" + } +} 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 f7802e86..e57d0de2 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -3,15 +3,8 @@ package com.futo.platformplayer.casting import android.app.AlertDialog import android.content.ContentResolver import android.content.Context -import android.net.Uri -import android.net.nsd.NsdManager -import android.net.nsd.NsdServiceInfo -import android.os.Build import android.os.Looper -import android.util.Base64 import android.util.Log -import java.net.NetworkInterface -import java.net.Inet4Address import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi import com.futo.platformplayer.R @@ -41,12 +34,12 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManif import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.builders.DashBuilder +import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.exceptions.UnsupportedCastException import com.futo.platformplayer.findPreferredAddress import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.parsers.HLS import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.stores.CastingDeviceInfoStorage @@ -54,280 +47,109 @@ import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.toUrlAddress import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json +import org.fcast.sender_sdk.Metadata import java.net.Inet6Address -import java.net.InetAddress import java.net.URLDecoder import java.net.URLEncoder -import java.util.Collections import java.util.UUID import java.util.concurrent.atomic.AtomicInteger -class StateCasting { - private val _scopeIO = CoroutineScope(Dispatchers.IO); - private val _scopeMain = CoroutineScope(Dispatchers.Main); - private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get(); - - private val _castServer = ManagedHttpServer(); - private var _started = false; - - var devices: HashMap = hashMapOf(); - val onDeviceAdded = Event1(); - val onDeviceChanged = Event1(); - val onDeviceRemoved = Event1(); - val onActiveDeviceConnectionStateChanged = Event2(); - val onActiveDevicePlayChanged = Event1(); - val onActiveDeviceTimeChanged = Event1(); - val onActiveDeviceDurationChanged = Event1(); - val onActiveDeviceVolumeChanged = Event1(); - var activeDevice: CastingDevice? = null; +abstract class StateCasting { + val _castServer = ManagedHttpServer() private var _videoExecutor: JSRequestExecutor? = null private var _audioExecutor: JSRequestExecutor? = null - private val _client = ManagedHttpClient(); - var _resumeCastingDevice: CastingDeviceInfo? = null; - private var _nsdManager: NsdManager? = null - val isCasting: Boolean get() = activeDevice != null; + val _scopeIO = CoroutineScope(Dispatchers.IO); + var _started = false; + private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get(); + val _client = ManagedHttpClient(); + var devices: HashMap = hashMapOf() + val onDeviceAdded = Event1() + val onDeviceChanged = Event1() + val onDeviceRemoved = Event1() + val onActiveDeviceConnectionStateChanged = Event2() + val onActiveDevicePlayChanged = Event1() + val onActiveDeviceTimeChanged = Event1() + val onActiveDeviceDurationChanged = Event1() + val onActiveDeviceVolumeChanged = Event1() + var activeDevice: CastingDevice? = null + val isCasting: Boolean get() = activeDevice != null + var _resumeCastingDevice: CastingDeviceInfo? = null + val _scopeMain = CoroutineScope(Dispatchers.Main) + private val _castingDialogLock = Any(); + private var _currentDialog: AlertDialog? = null; private val _castId = AtomicInteger(0) - private val _discoveryListeners = mapOf( - "_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice), - "_airplay._tcp" to createDiscoveryListener(::addOrUpdateAirPlayDevice), - "_fastcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice), - "_fcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice) - ) + abstract fun handleUrl(url: String) + abstract fun onStop() + abstract fun start(context: Context) + abstract fun stop() - fun handleUrl(context: Context, 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 - )) - - connectDevice(deviceFromCastingDeviceInfo(foundInfo)) - } - - 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.stop(); - } + @Throws + abstract fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice + abstract fun startUpdateTimeJob( + onTimeJobTimeChanged_s: Event1, setTime: (Long) -> Unit + ): Job? fun onResume() { val ad = activeDevice if (ad != null) { - if (ad is FCastCastingDevice) { - ad.ensureThreadStarted() - } else if (ad is ChromecastCastingDevice) { - ad.ensureThreadsStarted() - } + ad.ensureThreadStarted() } else { val resumeCastingDevice = _resumeCastingDevice if (resumeCastingDevice != null) { - connectDevice(deviceFromCastingDeviceInfo(resumeCastingDevice)) + val dev = deviceFromInfo(resumeCastingDevice) ?: return + connectDevice(dev) _resumeCastingDevice = null Log.i(TAG, "_resumeCastingDevice set to null onResume") } } } - @Synchronized - fun start(context: Context) { - if (_started) + fun cancel() { + _castId.incrementAndGet() + } + + fun invokeInMainScopeIfRequired(action: () -> Unit) { + if (Looper.getMainLooper().thread != Thread.currentThread()) { + _scopeMain.launch { action() } 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) - } } + + action(); } - @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 - 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?.stop(); - - _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) - } - }) - } - } - } - } - - private val _castingDialogLock = Any(); - private var _currentDialog: AlertDialog? = null; - @Synchronized fun connectDevice(device: CastingDevice) { - if (activeDevice == device) - return; + if (activeDevice == device) { + return + } val ad = activeDevice; if (ad != null) { Logger.i(TAG, "Stopping previous device because a new one is being connected.") - device.onConnectionStateChanged.clear(); - device.onPlayChanged.clear(); - device.onTimeChanged.clear(); - device.onVolumeChanged.clear(); - device.onDurationChanged.clear(); - ad.stop(); + device.onConnectionStateChanged.clear() + device.onPlayChanged.clear() + device.onTimeChanged.clear() + device.onVolumeChanged.clear() + device.onDurationChanged.clear() + ad.disconnect() } device.onConnectionStateChanged.subscribe { castConnectionState -> - Logger.i(TAG, "Active device connection state changed: $castConnectionState"); + Logger.i(TAG, "Active device connection state changed: $castConnectionState") if (castConnectionState == CastConnectionState.DISCONNECTED) { - Logger.i(TAG, "Clearing events: $castConnectionState"); + Logger.i(TAG, "Clearing events: $castConnectionState") - device.onConnectionStateChanged.clear(); - device.onPlayChanged.clear(); - device.onTimeChanged.clear(); - device.onVolumeChanged.clear(); - device.onDurationChanged.clear(); - activeDevice = null; + device.onConnectionStateChanged.clear() + device.onPlayChanged.clear() + device.onTimeChanged.clear() + device.onVolumeChanged.clear() + device.onDurationChanged.clear() + activeDevice = null } invokeInMainScopeIfRequired { @@ -339,112 +161,115 @@ class StateCasting { Logger.i(TAG, "Casting connected to [${device.name}]"); UIDialogs.appToast("Connected to device") synchronized(_castingDialogLock) { - if(_currentDialog != null) { - _currentDialog?.hide(); - _currentDialog = null; + if (_currentDialog != null) { + _currentDialog?.hide() + _currentDialog = null } } } + CastConnectionState.CONNECTING -> { Logger.i(TAG, "Casting connecting to [${device.name}]"); UIDialogs.toast(it, "Connecting to device...") synchronized(_castingDialogLock) { - if(_currentDialog == null) { - _currentDialog = UIDialogs.showDialog(context, R.drawable.ic_loader_animated, true, - "Connecting to [${device.name}]", - "Make sure you are on the same network\n\nVPNs and guest networks can cause issues", null, -2, + if (_currentDialog == null) { + _currentDialog = UIDialogs.showDialog( + context, + R.drawable.ic_loader_animated, + true, + "Connecting to [${device.name}]", + "Make sure you are on the same network\n\nVPNs and guest networks can cause issues", + null, + -2, UIDialogs.Action("Disconnect", { - device.stop(); - })); + try { + device.disconnect() + } catch (e: Throwable) { + Logger.e( + TAG, "Failed to disconnect from device: $e" + ) + } + }) + ) } } } + CastConnectionState.DISCONNECTED -> { UIDialogs.toast(it, "Disconnected from device") synchronized(_castingDialogLock) { - if(_currentDialog != null) { - _currentDialog?.hide(); - _currentDialog = null; + if (_currentDialog != null) { + _currentDialog?.hide() + _currentDialog = null } } } } } - }; - onActiveDeviceConnectionStateChanged.emit(device, castConnectionState); - }; - }; + } + onActiveDeviceConnectionStateChanged.emit(device, castConnectionState) + } + } device.onPlayChanged.subscribe { - invokeInMainScopeIfRequired { onActiveDevicePlayChanged.emit(it) }; + invokeInMainScopeIfRequired { onActiveDevicePlayChanged.emit(it) } } device.onDurationChanged.subscribe { - invokeInMainScopeIfRequired { onActiveDeviceDurationChanged.emit(it) }; - }; + invokeInMainScopeIfRequired { onActiveDeviceDurationChanged.emit(it) } + } device.onVolumeChanged.subscribe { - invokeInMainScopeIfRequired { onActiveDeviceVolumeChanged.emit(it) }; - }; + invokeInMainScopeIfRequired { onActiveDeviceVolumeChanged.emit(it) } + } device.onTimeChanged.subscribe { - invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) }; - }; + invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) } + } try { - device.start(); + device.connect(); } catch (e: Throwable) { - Logger.w(TAG, "Failed to connect to device."); - device.onConnectionStateChanged.clear(); - device.onPlayChanged.clear(); - device.onTimeChanged.clear(); - device.onVolumeChanged.clear(); - device.onDurationChanged.clear(); - return; + Logger.w(TAG, "Failed to connect to device.") + device.onConnectionStateChanged.clear() + device.onPlayChanged.clear() + device.onTimeChanged.clear() + device.onVolumeChanged.clear() + device.onDurationChanged.clear() + return } - activeDevice = device; - Logger.i(TAG, "Connect to device ${device.name}"); + activeDevice = device + Logger.i(TAG, "Connect to device ${device.name}") } - fun addRememberedDevice(deviceInfo: CastingDeviceInfo): CastingDeviceInfo { - val device = deviceFromCastingDeviceInfo(deviceInfo); - return addRememberedDevice(device); + fun metadataFromVideo(video: IPlatformVideoDetails): Metadata { + return Metadata( + title = video.name, thumbnailUrl = video.thumbnails.getHQThumbnail() + ) } - fun getRememberedCastingDevices(): List { - return _storage.getDevices().map { deviceFromCastingDeviceInfo(it) } + private fun shouldProxyStreams( + castingDevice: CastingDevice, videoSource: IVideoSource?, audioSource: IAudioSource? + ): Boolean { + val hasRequestModifier = + (videoSource as? JSSource)?.hasRequestModifier == true || (audioSource as? JSSource)?.hasRequestModifier == true + return Settings.instance.casting.alwaysProxyRequests || castingDevice.protocolType != CastProtocolType.FCAST || hasRequestModifier } - fun getRememberedCastingDeviceNames(): List { - return _storage.getDeviceNames() - } - - fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo { - val deviceInfo = device.getDeviceInfo() - return _storage.addDevice(deviceInfo) - } - - fun removeRememberedDevice(device: CastingDevice) { - val name = device.name ?: return - _storage.removeDevice(name) - } - - private fun invokeInMainScopeIfRequired(action: () -> Unit){ - if(Looper.getMainLooper().thread != Thread.currentThread()) { - _scopeMain.launch { action(); } - return; - } - - action(); - } - - fun cancel() { - _castId.incrementAndGet() - } - - suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null): Boolean { + suspend fun castIfAvailable( + contentResolver: ContentResolver, + video: IPlatformVideoDetails, + videoSource: IVideoSource?, + audioSource: IAudioSource?, + subtitleSource: ISubtitleSource?, + ms: Long, + speed: Double?, + onLoadingEstimate: ((Int) -> Unit)?, + onLoading: ((Boolean) -> Unit)? + ): Boolean { return withContext(Dispatchers.IO) { val ad = activeDevice ?: return@withContext false; if (ad.connectionState != CastConnectionState.CONNECTED) { return@withContext false; } + val deviceProto = ad.protocolType val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0; val castId = _castId.incrementAndGet() @@ -460,29 +285,79 @@ class StateCasting { if (sourceCount > 1) { if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) { - if (ad is AirPlayCastingDevice) { + if (deviceProto == CastProtocolType.AIRPLAY) { Logger.i(TAG, "Casting as local HLS"); - castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed); + castLocalHls( + video, + videoSource as LocalVideoSource?, + audioSource as LocalAudioSource?, + subtitleSource as LocalSubtitleSource?, + resumePosition, + speed + ); } else { Logger.i(TAG, "Casting as local DASH"); - castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition, speed); + castLocalDash( + video, + videoSource as LocalVideoSource?, + audioSource as LocalAudioSource?, + subtitleSource as LocalSubtitleSource?, + resumePosition, + speed + ); } } else { - val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource + val isRawDash = + videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource if (isRawDash) { Logger.i(TAG, "Casting as raw DASH"); - castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed, castId, onLoadingEstimate, onLoading); + castDashRaw( + contentResolver, + video, + videoSource as JSDashManifestRawSource?, + audioSource as JSDashManifestRawAudioSource?, + subtitleSource, + resumePosition, + speed, + castId, + onLoadingEstimate, + onLoading + ); } else { - if (ad is FCastCastingDevice) { + if (deviceProto == CastProtocolType.FCAST) { Logger.i(TAG, "Casting as DASH direct"); - castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed); - } else if (ad is AirPlayCastingDevice) { + castDashDirect( + contentResolver, + video, + videoSource as IVideoUrlSource?, + audioSource as IAudioUrlSource?, + subtitleSource, + resumePosition, + speed + ); + } else if (deviceProto == CastProtocolType.AIRPLAY) { Logger.i(TAG, "Casting as HLS indirect"); - castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed); + castHlsIndirect( + contentResolver, + video, + videoSource as IVideoUrlSource?, + audioSource as IAudioUrlSource?, + subtitleSource, + resumePosition, + speed + ); } else { Logger.i(TAG, "Casting as DASH indirect"); - castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed); + castDashIndirect( + contentResolver, + video, + videoSource as IVideoUrlSource?, + audioSource as IAudioUrlSource?, + subtitleSource, + resumePosition, + speed + ); } } } @@ -493,29 +368,65 @@ class StateCasting { if (videoSource is IVideoUrlSource) { val videoPath = "/video-${id}" - val videoUrl = if(proxyStreams) url + videoPath else videoSource.getVideoUrl(); + val videoUrl = if (proxyStreams) url + videoPath else videoSource.getVideoUrl(); Logger.i(TAG, "Casting as singular video"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed); + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + videoSource.container, + videoUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); } else if (audioSource is IAudioUrlSource) { val audioPath = "/audio-${id}" - val audioUrl = if(proxyStreams) url + audioPath else audioSource.getAudioUrl(); + val audioUrl = if (proxyStreams) url + audioPath else audioSource.getAudioUrl(); Logger.i(TAG, "Casting as singular audio"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed); - } else if(videoSource is IHLSManifestSource) { - if (proxyStreams || ad is ChromecastCastingDevice) { + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + audioSource.container, + audioUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); + } else if (videoSource is IHLSManifestSource) { + if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) { Logger.i(TAG, "Casting as proxied HLS"); - castProxiedHls(video, videoSource.url, videoSource.codec, resumePosition, speed); + castProxiedHls( + video, videoSource.url, videoSource.codec, resumePosition, speed + ); } else { Logger.i(TAG, "Casting as non-proxied HLS"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble(), speed); + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + videoSource.container, + videoSource.url, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); } - } else if(audioSource is IHLSManifestAudioSource) { - if (proxyStreams || ad is ChromecastCastingDevice) { + } else if (audioSource is IHLSManifestAudioSource) { + if (proxyStreams || deviceProto == CastProtocolType.CHROMECAST) { Logger.i(TAG, "Casting as proxied audio HLS"); - castProxiedHls(video, audioSource.url, audioSource.codec, resumePosition, speed); + castProxiedHls( + video, audioSource.url, audioSource.codec, resumePosition, speed + ); } else { Logger.i(TAG, "Casting as non-proxied audio HLS"); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble(), speed); + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + audioSource.container, + audioSource.url, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); } } else if (videoSource is LocalVideoSource) { Logger.i(TAG, "Casting as local video"); @@ -525,15 +436,37 @@ class StateCasting { castLocalAudio(video, audioSource, resumePosition, speed); } else if (videoSource is JSDashManifestRawSource) { Logger.i(TAG, "Casting as JSDashManifestRawSource video"); - castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed, castId, onLoadingEstimate, onLoading); + castDashRaw( + contentResolver, + video, + videoSource as JSDashManifestRawSource?, + null, + null, + resumePosition, + speed, + castId, + onLoadingEstimate, + onLoading + ); } else if (audioSource is JSDashManifestRawAudioSource) { Logger.i(TAG, "Casting as JSDashManifestRawSource audio"); - castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed, castId, onLoadingEstimate, onLoading); + castDashRaw( + contentResolver, + video, + null, + audioSource as JSDashManifestRawAudioSource?, + null, + resumePosition, + speed, + castId, + onLoadingEstimate, + onLoading + ); } else { var str = listOf( - if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, - if(audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null, - if(subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null + if (videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, + if (audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null, + if (subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null ).filterNotNull().joinToString(", "); throw UnsupportedCastException(str); } @@ -545,29 +478,46 @@ class StateCasting { fun resumeVideo(): Boolean { val ad = activeDevice ?: return false; - ad.resumeVideo(); + try { + ad.resumePlayback(); + } catch (_: Throwable) { + } return true; } fun pauseVideo(): Boolean { val ad = activeDevice ?: return false; - ad.pauseVideo(); + try { + ad.pausePlayback(); + } catch (_: Throwable) { + } return true; } fun stopVideo(): Boolean { val ad = activeDevice ?: return false; - ad.stopVideo(); + try { + ad.stopPlayback(); + } catch (_: Throwable) { + } return true; } fun videoSeekTo(timeSeconds: Double): Boolean { val ad = activeDevice ?: return false; - ad.seekVideo(timeSeconds); + try { + ad.seekTo(timeSeconds); + } catch (_: Throwable) { + } return true; } - private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List { + private fun castLocalVideo( + video: IPlatformVideoDetails, + videoSource: LocalVideoSource, + resumePosition: Double, + speed: Double? + ): List { val ad = activeDevice ?: return listOf(); val url = getLocalUrl(ad); @@ -576,17 +526,34 @@ class StateCasting { val videoUrl = url + videoPath; _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpFileHandler( + "GET", + videoPath, + videoSource.container, + videoSource.filePath + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); Logger.i(TAG, "Casting local video (videoUrl: $videoUrl)."); - ad.loadVideo("BUFFERED", videoSource.container, videoUrl, resumePosition, video.duration.toDouble(), speed); + ad.loadVideo( + "BUFFERED", + videoSource.container, + videoUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); return listOf(videoUrl); } - private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List { + private fun castLocalAudio( + video: IPlatformVideoDetails, + audioSource: LocalAudioSource, + resumePosition: Double, + speed: Double? + ): List { val ad = activeDevice ?: return listOf(); val url = getLocalUrl(ad); @@ -595,17 +562,36 @@ class StateCasting { val audioUrl = url + audioPath; _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpFileHandler( + "GET", + audioPath, + audioSource.container, + audioSource.filePath + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); Logger.i(TAG, "Casting local audio (audioUrl: $audioUrl)."); - ad.loadVideo("BUFFERED", audioSource.container, audioUrl, resumePosition, video.duration.toDouble(), speed); + ad.loadVideo( + "BUFFERED", + audioSource.container, + audioUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); return listOf(audioUrl); } - private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List { + private fun castLocalHls( + video: IPlatformVideoDetails, + videoSource: LocalVideoSource?, + audioSource: LocalAudioSource?, + subtitleSource: LocalSubtitleSource?, + resumePosition: Double, + speed: Double? + ): List { val ad = activeDevice ?: return listOf() val url = getLocalUrl(ad) @@ -626,82 +612,161 @@ class StateCasting { if (videoSource != null) { _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpFileHandler( + "GET", + videoPath, + videoSource.container, + videoSource.filePath + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castLocalHls") val duration = videoSource.duration val videoVariantPlaylistPath = "/video-playlist-${id}" val videoVariantPlaylistUrl = url + videoVariantPlaylistPath - val videoVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), videoUrl)) - val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, videoVariantPlaylistSegments) + val videoVariantPlaylistSegments = + listOf(HLS.MediaSegment(duration.toDouble(), videoUrl)) + val videoVariantPlaylist = HLS.VariantPlaylist( + 3, duration.toInt(), 0, 0, null, null, null, videoVariantPlaylistSegments + ) _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", + videoVariantPlaylistPath, + videoVariantPlaylist.buildM3U8(), + "application/vnd.apple.mpegurl" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castLocalHls") - variantPlaylistReferences.add(HLS.VariantPlaylistReference(videoVariantPlaylistUrl, HLS.StreamInfo( - videoSource.bitrate, "${videoSource.width}x${videoSource.height}", videoSource.codec, null, null, if (audioSource != null) "audio" else null, if (subtitleSource != null) "subtitles" else null, null, null))) + variantPlaylistReferences.add( + HLS.VariantPlaylistReference( + videoVariantPlaylistUrl, HLS.StreamInfo( + videoSource.bitrate, + "${videoSource.width}x${videoSource.height}", + videoSource.codec, + null, + null, + if (audioSource != null) "audio" else null, + if (subtitleSource != null) "subtitles" else null, + null, + null + ) + ) + ) } if (audioSource != null) { _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpFileHandler( + "GET", + audioPath, + audioSource.container, + audioSource.filePath + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castLocalHls") - val duration = audioSource.duration ?: videoSource?.duration ?: throw Exception("Duration unknown") + val duration = + audioSource.duration ?: videoSource?.duration ?: throw Exception("Duration unknown") val audioVariantPlaylistPath = "/audio-playlist-${id}" val audioVariantPlaylistUrl = url + audioVariantPlaylistPath - val audioVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), audioUrl)) - val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, audioVariantPlaylistSegments) + val audioVariantPlaylistSegments = + listOf(HLS.MediaSegment(duration.toDouble(), audioUrl)) + val audioVariantPlaylist = HLS.VariantPlaylist( + 3, duration.toInt(), 0, 0, null, null, null, audioVariantPlaylistSegments + ) _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", + audioVariantPlaylistPath, + audioVariantPlaylist.buildM3U8(), + "application/vnd.apple.mpegurl" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castLocalHls") - mediaRenditions.add(HLS.MediaRendition("AUDIO", audioVariantPlaylistUrl, "audio", "df", "default", true, true, true)) + mediaRenditions.add( + HLS.MediaRendition( + "AUDIO", audioVariantPlaylistUrl, "audio", "df", "default", true, true, true + ) + ) } if (subtitleSource != null) { _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", subtitlePath, subtitleSource.format ?: "text/vtt", subtitleSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpFileHandler( + "GET", + subtitlePath, + subtitleSource.format ?: "text/vtt", + subtitleSource.filePath + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castLocalHls") - val duration = videoSource?.duration ?: audioSource?.duration ?: throw Exception("Duration unknown") + val duration = videoSource?.duration ?: audioSource?.duration + ?: throw Exception("Duration unknown") val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}" val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath - val subtitleVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), subtitleUrl)) - val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, subtitleVariantPlaylistSegments) + val subtitleVariantPlaylistSegments = + listOf(HLS.MediaSegment(duration.toDouble(), subtitleUrl)) + val subtitleVariantPlaylist = HLS.VariantPlaylist( + 3, duration.toInt(), 0, 0, null, null, null, subtitleVariantPlaylistSegments + ) _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", + subtitleVariantPlaylistPath, + subtitleVariantPlaylist.buildM3U8(), + "application/vnd.apple.mpegurl" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castLocalHls") - mediaRenditions.add(HLS.MediaRendition("SUBTITLES", subtitleVariantPlaylistUrl, "subtitles", "df", "default", true, true, true)) + mediaRenditions.add( + HLS.MediaRendition( + "SUBTITLES", + subtitleVariantPlaylistUrl, + "subtitles", + "df", + "default", + true, + true, + true + ) + ) } - val masterPlaylist = HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true) + val masterPlaylist = + HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true) _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", hlsPath, masterPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", hlsPath, masterPlaylist.buildM3U8(), "application/vnd.apple.mpegurl" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castLocalHls") - Logger.i(TAG, "added new castLocalHls handlers (hlsPath: $hlsPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath).") - ad.loadVideo("BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), speed) + Logger.i( + TAG, + "added new castLocalHls handlers (hlsPath: $hlsPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath)." + ) + ad.loadVideo( + "BUFFERED", + "application/vnd.apple.mpegurl", + hlsUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ) return listOf(hlsUrl, videoUrl, audioUrl, subtitleUrl) } - private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List { + private fun castLocalDash( + video: IPlatformVideoDetails, + videoSource: LocalVideoSource?, + audioSource: LocalAudioSource?, + subtitleSource: LocalSubtitleSource?, + resumePosition: Double, + speed: Double? + ): List { val ad = activeDevice ?: return listOf(); val url = getLocalUrl(ad); @@ -717,40 +782,73 @@ class StateCasting { val audioUrl = url + audioPath; val subtitleUrl = url + subtitlePath; - val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl); + val dashContent = DashBuilder.generateOnDemandDash( + videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitleUrl + ); Logger.v(TAG) { "Dash manifest: $dashContent" }; _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", dashPath, dashContent, - "application/dash+xml") - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); + HttpConstantHandler( + "GET", dashPath, dashContent, "application/dash+xml" + ).withHeader("Access-Control-Allow-Origin", "*"), true + ).withTag("cast"); if (videoSource != null) { _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpFileHandler( + "GET", + videoPath, + videoSource.container, + videoSource.filePath + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); } if (audioSource != null) { _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpFileHandler( + "GET", + audioPath, + audioSource.container, + audioSource.filePath + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); } if (subtitleSource != null) { _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", subtitlePath, subtitleSource.format ?: "text/vtt", subtitleSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpFileHandler( + "GET", + subtitlePath, + subtitleSource.format ?: "text/vtt", + subtitleSource.filePath + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); } - Logger.i(TAG, "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath)."); - ad.loadVideo("BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed); + Logger.i( + TAG, + "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath)." + ); + ad.loadVideo( + "BUFFERED", + "application/dash+xml", + dashUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); return listOf(dashUrl, videoUrl, audioUrl, subtitleUrl); } - private suspend fun castDashDirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List { + private suspend fun castDashDirect( + contentResolver: ContentResolver, + video: IPlatformVideoDetails, + videoSource: IVideoUrlSource?, + audioSource: IAudioUrlSource?, + subtitleSource: ISubtitleSource?, + resumePosition: Double, + speed: Double? + ): List { val ad = activeDevice ?: return listOf(); val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource) @@ -761,8 +859,8 @@ class StateCasting { val audioPath = "/audio-${id}" val subtitlePath = "/subtitle-${id}" - val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl(); - val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl(); + val videoUrl = if (proxyStreams) url + videoPath else videoSource?.getVideoUrl(); + val audioUrl = if (proxyStreams) url + audioPath else audioSource?.getAudioUrl(); val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) { return@withContext subtitleSource.getSubtitlesURI(); @@ -770,7 +868,7 @@ class StateCasting { var subtitlesUrl: String? = null; if (subtitlesUri != null) { - if(subtitlesUri.scheme == "file") { + if (subtitlesUri.scheme == "file") { var content: String? = null; val inputStream = contentResolver.openInputStream(subtitlesUri); inputStream?.use { stream -> @@ -780,8 +878,9 @@ class StateCasting { if (content != null) { _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); } @@ -793,29 +892,59 @@ class StateCasting { if (videoSource != null) { _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpProxyHandler( + "GET", + videoPath, + videoSource.getVideoUrl(), + true + ).withInjectedHost().withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); } if (audioSource != null) { _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpProxyHandler( + "GET", + audioPath, + audioSource.getAudioUrl(), + true + ).withInjectedHost().withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); } - val content = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl); + val content = DashBuilder.generateOnDemandDash( + videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl + ); - Logger.i(TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl)."); + Logger.i( + TAG, "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl)." + ); Logger.v(TAG) { "Dash manifest: $content" }; - ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed); + ad.loadContent( + "application/dash+xml", + content, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); - return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString()); + return listOf( + videoUrl ?: "", + audioUrl ?: "", + subtitlesUrl ?: "", + videoSource?.getVideoUrl() ?: "", + audioSource?.getAudioUrl() ?: "", + subtitlesUri.toString() + ); } - private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List { + private fun castProxiedHls( + video: IPlatformVideoDetails, + sourceUrl: String, + codec: String?, + resumePosition: Double, + speed: Double? + ): List { _castServer.removeAllHandlers("castProxiedHlsMaster") val ad = activeDevice ?: return listOf(); @@ -826,122 +955,164 @@ class StateCasting { val hlsUrl = url + hlsPath Logger.i(TAG, "HLS url: $hlsUrl"); - _castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", hlsPath) { masterContext -> - _castServer.removeAllHandlers("castProxiedHlsVariant") + _castServer.addHandlerWithAllowAllOptions( + HttpFunctionHandler( + "GET", hlsPath + ) { masterContext -> + _castServer.removeAllHandlers("castProxiedHlsVariant") - val headers = masterContext.headers.clone() - headers["Content-Type"] = "application/vnd.apple.mpegurl"; + val headers = masterContext.headers.clone() + headers["Content-Type"] = "application/vnd.apple.mpegurl"; - val masterPlaylistResponse = _client.get(sourceUrl) - check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" } + val masterPlaylistResponse = _client.get(sourceUrl) + check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" } - val masterPlaylistContent = masterPlaylistResponse.body?.string() - ?: throw Exception("Master playlist content is empty") + val masterPlaylistContent = masterPlaylistResponse.body?.string() + ?: throw Exception("Master playlist content is empty") - val masterPlaylist: HLS.MasterPlaylist - try { - masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl) - } catch (e: Throwable) { - if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) { - //This is a variant playlist, not a master playlist - Logger.i(TAG, "HLS casting as variant playlist (codec: $codec): $hlsUrl"); + val masterPlaylist: HLS.MasterPlaylist + try { + masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl) + } catch (e: Throwable) { + if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) { + //This is a variant playlist, not a master playlist + Logger.i(TAG, "HLS casting as variant playlist (codec: $codec): $hlsUrl"); - val vpHeaders = masterContext.headers.clone() - vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; - - val variantPlaylist = HLS.parseVariantPlaylist(masterPlaylistContent, sourceUrl) - val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive) - val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() - masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); - return@HttpFunctionHandler - } else { - throw e - } - } - - Logger.i(TAG, "HLS casting as master playlist: $hlsUrl"); - - val newVariantPlaylistRefs = arrayListOf() - val newMediaRenditions = arrayListOf() - val newMasterPlaylist = HLS.MasterPlaylist(newVariantPlaylistRefs, newMediaRenditions, masterPlaylist.sessionDataList, masterPlaylist.independentSegments) - - for (variantPlaylistRef in masterPlaylist.variantPlaylistsRefs) { - val playlistId = UUID.randomUUID(); - val newPlaylistPath = "/hls-playlist-${playlistId}" - val newPlaylistUrl = url + newPlaylistPath; - - _castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext -> - val vpHeaders = vpContext.headers.clone() - vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; - - val response = _client.get(variantPlaylistRef.url) - check(response.isOk) { "Failed to get variant playlist: ${response.code}" } - - val vpContent = response.body?.string() - ?: throw Exception("Variant playlist content is empty") - - val variantPlaylist = HLS.parseVariantPlaylist(vpContent, variantPlaylistRef.url) - val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive) - val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() - vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); - }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castProxiedHlsVariant") - - newVariantPlaylistRefs.add(HLS.VariantPlaylistReference( - newPlaylistUrl, - variantPlaylistRef.streamInfo - )) - } - - for (mediaRendition in masterPlaylist.mediaRenditions) { - val playlistId = UUID.randomUUID() - - var newPlaylistUrl: String? = null - if (mediaRendition.uri != null) { - val newPlaylistPath = "/hls-playlist-${playlistId}" - newPlaylistUrl = url + newPlaylistPath - - _castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext -> - val vpHeaders = vpContext.headers.clone() + val vpHeaders = masterContext.headers.clone() vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; - val response = _client.get(mediaRendition.uri) - check(response.isOk) { "Failed to get variant playlist: ${response.code}" } - - val vpContent = response.body?.string() - ?: throw Exception("Variant playlist content is empty") - - val variantPlaylist = HLS.parseVariantPlaylist(vpContent, mediaRendition.uri) - val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive) + val variantPlaylist = + HLS.parseVariantPlaylist(masterPlaylistContent, sourceUrl) + val proxiedVariantPlaylist = + proxyVariantPlaylist(url, id, variantPlaylist, video.isLive) val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() - vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); - }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castProxiedHlsVariant") + masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); + return@HttpFunctionHandler + } else { + throw e + } } - newMediaRenditions.add(HLS.MediaRendition( - mediaRendition.type, - newPlaylistUrl, - mediaRendition.groupID, - mediaRendition.language, - mediaRendition.name, - mediaRendition.isDefault, - mediaRendition.isAutoSelect, - mediaRendition.isForced - )) - } + Logger.i(TAG, "HLS casting as master playlist: $hlsUrl"); - masterContext.respondCode(200, headers, newMasterPlaylist.buildM3U8()); - }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castProxiedHlsMaster") + val newVariantPlaylistRefs = arrayListOf() + val newMediaRenditions = arrayListOf() + val newMasterPlaylist = HLS.MasterPlaylist( + newVariantPlaylistRefs, + newMediaRenditions, + masterPlaylist.sessionDataList, + masterPlaylist.independentSegments + ) + + for (variantPlaylistRef in masterPlaylist.variantPlaylistsRefs) { + val playlistId = UUID.randomUUID(); + val newPlaylistPath = "/hls-playlist-${playlistId}" + val newPlaylistUrl = url + newPlaylistPath; + + _castServer.addHandlerWithAllowAllOptions( + HttpFunctionHandler( + "GET", newPlaylistPath + ) { vpContext -> + val vpHeaders = vpContext.headers.clone() + vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; + + val response = _client.get(variantPlaylistRef.url) + check(response.isOk) { "Failed to get variant playlist: ${response.code}" } + + val vpContent = response.body?.string() + ?: throw Exception("Variant playlist content is empty") + + val variantPlaylist = + HLS.parseVariantPlaylist(vpContent, variantPlaylistRef.url) + val proxiedVariantPlaylist = + proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive) + val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() + vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); + }.withHeader("Access-Control-Allow-Origin", "*"), true + ).withTag("castProxiedHlsVariant") + + newVariantPlaylistRefs.add( + HLS.VariantPlaylistReference( + newPlaylistUrl, variantPlaylistRef.streamInfo + ) + ) + } + + for (mediaRendition in masterPlaylist.mediaRenditions) { + val playlistId = UUID.randomUUID() + + var newPlaylistUrl: String? = null + if (mediaRendition.uri != null) { + val newPlaylistPath = "/hls-playlist-${playlistId}" + newPlaylistUrl = url + newPlaylistPath + + _castServer.addHandlerWithAllowAllOptions( + HttpFunctionHandler( + "GET", newPlaylistPath + ) { vpContext -> + val vpHeaders = vpContext.headers.clone() + vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; + + val response = _client.get(mediaRendition.uri) + check(response.isOk) { "Failed to get variant playlist: ${response.code}" } + + val vpContent = response.body?.string() + ?: throw Exception("Variant playlist content is empty") + + val variantPlaylist = + HLS.parseVariantPlaylist(vpContent, mediaRendition.uri) + val proxiedVariantPlaylist = proxyVariantPlaylist( + url, playlistId, variantPlaylist, video.isLive + ) + val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() + vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); + }.withHeader("Access-Control-Allow-Origin", "*"), true + ).withTag("castProxiedHlsVariant") + } + + newMediaRenditions.add( + HLS.MediaRendition( + mediaRendition.type, + newPlaylistUrl, + mediaRendition.groupID, + mediaRendition.language, + mediaRendition.name, + mediaRendition.isDefault, + mediaRendition.isAutoSelect, + mediaRendition.isForced + ) + ) + } + + masterContext.respondCode(200, headers, newMasterPlaylist.buildM3U8()); + }.withHeader("Access-Control-Allow-Origin", "*"), true + ).withTag("castProxiedHlsMaster") Logger.i(TAG, "added new castHlsIndirect handlers (hlsPath: $hlsPath)."); //ChromeCast is sometimes funky with resume position 0 - val hackfixResumePosition = if (ad is ChromecastCastingDevice && !video.isLive && resumePosition == 0.0) 0.1 else resumePosition; - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, hackfixResumePosition, video.duration.toDouble(), speed); + val hackfixResumePosition = + if (ad.protocolType == CastProtocolType.CHROMECAST && !video.isLive && resumePosition == 0.0) 0.1 else resumePosition; + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + "application/vnd.apple.mpegurl", + hlsUrl, + hackfixResumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); return listOf(hlsUrl); } - private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, isLive: Boolean, proxySegments: Boolean = true): HLS.VariantPlaylist { + private fun proxyVariantPlaylist( + url: String, + playlistId: UUID, + variantPlaylist: HLS.VariantPlaylist, + isLive: Boolean, + proxySegments: Boolean = true + ): HLS.VariantPlaylist { val newSegments = arrayListOf() if (proxySegments) { @@ -965,29 +1136,37 @@ class StateCasting { ) } - private fun proxySegment(url: String, playlistId: UUID, segment: HLS.Segment, index: Long): HLS.Segment { + private fun proxySegment( + url: String, playlistId: UUID, segment: HLS.Segment, index: Long + ): HLS.Segment { if (segment is HLS.MediaSegment) { val newSegmentPath = "/hls-playlist-${playlistId}-segment-${index}" val newSegmentUrl = url + newSegmentPath; if (_castServer.getHandler("GET", newSegmentPath) == null) { _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", newSegmentPath, segment.uri, true) - .withInjectedHost() + HttpProxyHandler("GET", newSegmentPath, segment.uri, true).withInjectedHost() .withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castProxiedHlsVariant") } return HLS.MediaSegment( - segment.duration, - newSegmentUrl + segment.duration, newSegmentUrl ) } else { return segment } } - private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List { + private suspend fun castHlsIndirect( + contentResolver: ContentResolver, + video: IPlatformVideoDetails, + videoSource: IVideoUrlSource?, + audioSource: IAudioUrlSource?, + subtitleSource: ISubtitleSource?, + resumePosition: Double, + speed: Double? + ): List { val ad = activeDevice ?: return listOf(); val url = getLocalUrl(ad); val id = UUID.randomUUID(); @@ -1004,24 +1183,38 @@ class StateCasting { val audioPath = "/audio-${id}" val audioUrl = url + audioPath - val duration = audioSource.duration ?: videoSource?.duration ?: throw Exception("Duration unknown") + val duration = + audioSource.duration ?: videoSource?.duration ?: throw Exception("Duration unknown") val audioVariantPlaylistPath = "/audio-playlist-${id}" val audioVariantPlaylistUrl = url + audioVariantPlaylistPath - val audioVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), audioUrl)) - val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, audioVariantPlaylistSegments) + val audioVariantPlaylistSegments = + listOf(HLS.MediaSegment(duration.toDouble(), audioUrl)) + val audioVariantPlaylist = HLS.VariantPlaylist( + 3, duration.toInt(), 0, 0, null, null, null, audioVariantPlaylistSegments + ) _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", + audioVariantPlaylistPath, + audioVariantPlaylist.buildM3U8(), + "application/vnd.apple.mpegurl" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castHlsIndirectVariant"); - mediaRenditions.add(HLS.MediaRendition("AUDIO", audioVariantPlaylistUrl, "audio", "df", "default", true, true, true)) + mediaRenditions.add( + HLS.MediaRendition( + "AUDIO", audioVariantPlaylistUrl, "audio", "df", "default", true, true, true + ) + ) _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpProxyHandler( + "GET", + audioPath, + audioSource.getAudioUrl(), + true + ).withInjectedHost().withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castHlsIndirectVariant"); } @@ -1032,7 +1225,7 @@ class StateCasting { var subtitlesUrl: String? = null; if (subtitlesUri != null) { val subtitlePath = "/subtitles-${id}" - if(subtitlesUri.scheme == "file") { + if (subtitlesUri.scheme == "file") { var content: String? = null; val inputStream = contentResolver.openInputStream(subtitlesUri); inputStream?.use { stream -> @@ -1042,8 +1235,9 @@ class StateCasting { if (content != null) { _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castHlsIndirectVariant"); } @@ -1054,19 +1248,37 @@ class StateCasting { } if (subtitlesUrl != null) { - val duration = videoSource?.duration ?: audioSource?.duration ?: throw Exception("Duration unknown") + val duration = videoSource?.duration ?: audioSource?.duration + ?: throw Exception("Duration unknown") val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}" val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath - val subtitleVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), subtitlesUrl)) - val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, subtitleVariantPlaylistSegments) + val subtitleVariantPlaylistSegments = + listOf(HLS.MediaSegment(duration.toDouble(), subtitlesUrl)) + val subtitleVariantPlaylist = HLS.VariantPlaylist( + 3, duration.toInt(), 0, 0, null, null, null, subtitleVariantPlaylistSegments + ) _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", + subtitleVariantPlaylistPath, + subtitleVariantPlaylist.buildM3U8(), + "application/vnd.apple.mpegurl" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castHlsIndirectVariant"); - mediaRenditions.add(HLS.MediaRendition("SUBTITLES", subtitleVariantPlaylistUrl, "subtitles", "df", "default", true, true, true)) + mediaRenditions.add( + HLS.MediaRendition( + "SUBTITLES", + subtitleVariantPlaylistUrl, + "subtitles", + "df", + "default", + true, + true, + true + ) + ) } if (videoSource != null) { @@ -1076,51 +1288,83 @@ class StateCasting { val duration = videoSource.duration val videoVariantPlaylistPath = "/video-playlist-${id}" val videoVariantPlaylistUrl = url + videoVariantPlaylistPath - val videoVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), videoUrl)) - val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, null, videoVariantPlaylistSegments) + val videoVariantPlaylistSegments = + listOf(HLS.MediaSegment(duration.toDouble(), videoUrl)) + val videoVariantPlaylist = HLS.VariantPlaylist( + 3, duration.toInt(), 0, 0, null, null, null, videoVariantPlaylistSegments + ) _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", + videoVariantPlaylistPath, + videoVariantPlaylist.buildM3U8(), + "application/vnd.apple.mpegurl" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castHlsIndirectVariant"); - variantPlaylistReferences.add(HLS.VariantPlaylistReference(videoVariantPlaylistUrl, HLS.StreamInfo( - videoSource.bitrate ?: 0, - "${videoSource.width}x${videoSource.height}", - videoSource.codec, - null, - null, - if (audioSource != null) "audio" else null, - if (subtitleSource != null) "subtitles" else null, - null, null))) + variantPlaylistReferences.add( + HLS.VariantPlaylistReference( + videoVariantPlaylistUrl, HLS.StreamInfo( + videoSource.bitrate ?: 0, + "${videoSource.width}x${videoSource.height}", + videoSource.codec, + null, + null, + if (audioSource != null) "audio" else null, + if (subtitleSource != null) "subtitles" else null, + null, + null + ) + ) + ) _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpProxyHandler( + "GET", + videoPath, + videoSource.getVideoUrl(), + true + ).withInjectedHost().withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castHlsIndirectVariant"); } - val masterPlaylist = HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true) + val masterPlaylist = + HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true) _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", hlsPath, masterPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", hlsPath, masterPlaylist.buildM3U8(), "application/vnd.apple.mpegurl" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castHlsIndirectMaster") Logger.i(TAG, "added new castHls handlers (hlsPath: $hlsPath)."); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble(), speed); + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + "application/vnd.apple.mpegurl", + hlsUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); - return listOf(hlsUrl, videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString()); + return listOf( + hlsUrl, + videoSource?.getVideoUrl() ?: "", + audioSource?.getAudioUrl() ?: "", + subtitlesUri.toString() + ); } - private fun shouldProxyStreams(castingDevice: CastingDevice, videoSource: IVideoSource?, audioSource: IAudioSource?): Boolean { - val hasRequestModifier = (videoSource as? JSSource)?.hasRequestModifier == true || (audioSource as? JSSource)?.hasRequestModifier == true - return Settings.instance.casting.alwaysProxyRequests || castingDevice !is FCastCastingDevice || hasRequestModifier - } - - private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List { + private suspend fun castDashIndirect( + contentResolver: ContentResolver, + video: IPlatformVideoDetails, + videoSource: IVideoUrlSource?, + audioSource: IAudioUrlSource?, + subtitleSource: ISubtitleSource?, + resumePosition: Double, + speed: Double? + ): List { val ad = activeDevice ?: return listOf(); val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource) @@ -1135,8 +1379,8 @@ class StateCasting { val dashUrl = url + dashPath; Logger.i(TAG, "DASH url: $dashUrl"); - val videoUrl = if(proxyStreams) url + videoPath else videoSource?.getVideoUrl(); - val audioUrl = if(proxyStreams) url + audioPath else audioSource?.getAudioUrl(); + val videoUrl = if (proxyStreams) url + videoPath else videoSource?.getVideoUrl(); + val audioUrl = if (proxyStreams) url + audioPath else audioSource?.getAudioUrl(); val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) { return@withContext subtitleSource.getSubtitlesURI(); @@ -1147,7 +1391,7 @@ class StateCasting { var subtitlesUrl: String? = null; if (subtitlesUri != null) { - if(subtitlesUri.scheme == "file") { + if (subtitlesUri.scheme == "file") { var content: String? = null; val inputStream = contentResolver.openInputStream(subtitlesUri); inputStream?.use { stream -> @@ -1157,8 +1401,9 @@ class StateCasting { if (content != null) { _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); } @@ -1168,37 +1413,64 @@ class StateCasting { } } - val dashContent = DashBuilder.generateOnDemandDash(videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl); + val dashContent = DashBuilder.generateOnDemandDash( + videoSource, videoUrl, audioSource, audioUrl, subtitleSource, subtitlesUrl + ); Logger.v(TAG) { "Dash manifest: $dashContent" }; _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", dashPath, dashContent, - "application/dash+xml") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", dashPath, dashContent, "application/dash+xml" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); if (videoSource != null) { _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpProxyHandler( + "GET", + videoPath, + videoSource.getVideoUrl(), + true + ).withInjectedHost().withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); } if (audioSource != null) { _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpProxyHandler( + "GET", + audioPath, + audioSource.getAudioUrl(), + true + ).withInjectedHost().withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); } - Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath)."); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed); + Logger.i( + TAG, + "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath)." + ); + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + "application/dash+xml", + dashUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); - return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString()); + return listOf( + dashUrl, + videoUrl ?: "", + audioUrl ?: "", + subtitlesUrl ?: "", + videoSource?.getVideoUrl() ?: "", + audioSource?.getAudioUrl() ?: "", + subtitlesUri.toString() + ); } - private fun cleanExecutors() { + fun cleanExecutors() { if (_videoExecutor != null) { _videoExecutor?.cleanup() _videoExecutor = null @@ -1210,7 +1482,7 @@ class StateCasting { } } - private fun getLocalUrl(ad: CastingDevice): String { + fun getLocalUrl(ad: CastingDevice): String { var address = ad.localAddress!! if (Settings.instance.casting.allowLinkLocalIpv4) { if (address.isLinkLocalAddress && address is Inet6Address) { @@ -1227,7 +1499,18 @@ class StateCasting { } @OptIn(UnstableApi::class) - private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?, castId: Int, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null) : List { + suspend fun castDashRaw( + contentResolver: ContentResolver, + video: IPlatformVideoDetails, + videoSource: JSDashManifestRawSource?, + audioSource: JSDashManifestRawAudioSource?, + subtitleSource: ISubtitleSource?, + resumePosition: Double, + speed: Double?, + castId: Int, + onLoadingEstimate: ((Int) -> Unit)? = null, + onLoading: ((Boolean) -> Unit)? = null + ): List { val ad = activeDevice ?: return listOf(); cleanExecutors() @@ -1253,7 +1536,7 @@ class StateCasting { var subtitlesUrl: String? = null; if (subtitlesUri != null) { - if(subtitlesUri.scheme == "file") { + if (subtitlesUri.scheme == "file") { var content: String? = null; val inputStream = contentResolver.openInputStream(subtitlesUri); inputStream?.use { stream -> @@ -1263,8 +1546,9 @@ class StateCasting { if (content != null) { _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("cast"); } @@ -1317,16 +1601,25 @@ class StateCasting { } for (representation in representationRegex.findAll(dashContent)) { - val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found") + val mediaType = + representation.groups[1]?.value ?: throw Exception("Media type should be found") dashContent = mediaInitializationRegex.replace(dashContent) { if (it.range.first < representation.range.first || it.range.last > representation.range.last) { return@replace it.value } if (mediaType.startsWith("video/")) { - return@replace "${it.groups[1]!!.value}=\"${videoUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\"" + return@replace "${it.groups[1]!!.value}=\"${videoUrl}?url=${ + URLEncoder.encode( + it.groups[2]!!.value, "UTF-8" + ).replace("%24Number%24", "\$Number\$") + }&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\"" } else if (mediaType.startsWith("audio/")) { - return@replace "${it.groups[1]!!.value}=\"${audioUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\"" + return@replace "${it.groups[1]!!.value}=\"${audioUrl}?url=${ + URLEncoder.encode( + it.groups[2]!!.value, "UTF-8" + ).replace("%24Number%24", "\$Number\$") + }&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\"" } else { throw Exception("Expected audio or video") } @@ -1354,20 +1647,26 @@ class StateCasting { Logger.v(TAG) { "Dash manifest: $dashContent" }; _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler("GET", dashPath, dashContent, - "application/dash+xml") - .withHeader("Access-Control-Allow-Origin", "*"), true + HttpConstantHandler( + "GET", dashPath, dashContent, "application/dash+xml" + ).withHeader("Access-Control-Allow-Origin", "*"), true ).withTag("castDashRaw"); if (videoSource != null) { _castServer.addHandlerWithAllowAllOptions( HttpFunctionHandler("GET", videoPath) { httpContext -> - val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler - val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler + val originalUrl = + httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } + ?: return@HttpFunctionHandler + val mediaType = + httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } + ?: return@HttpFunctionHandler val videoExecutor = _videoExecutor; if (videoExecutor != null) { - val data = videoExecutor.executeRequest("GET", originalUrl, null, httpContext.headers) + val data = videoExecutor.executeRequest( + "GET", originalUrl, null, httpContext.headers + ) httpContext.respondBytes(200, HttpHeaders().apply { put("Content-Type", mediaType) }, data); @@ -1380,12 +1679,18 @@ class StateCasting { if (audioSource != null) { _castServer.addHandlerWithAllowAllOptions( HttpFunctionHandler("GET", audioPath) { httpContext -> - val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler - val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler + val originalUrl = + httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } + ?: return@HttpFunctionHandler + val mediaType = + httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } + ?: return@HttpFunctionHandler val audioExecutor = _audioExecutor; if (audioExecutor != null) { - val data = audioExecutor.executeRequest("GET", originalUrl, null, httpContext.headers) + val data = audioExecutor.executeRequest( + "GET", originalUrl, null, httpContext.headers + ) httpContext.respondBytes(200, HttpHeaders().apply { put("Content-Type", mediaType) }, data); @@ -1396,164 +1701,73 @@ class StateCasting { ).withTag("castDashRaw"); } - Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath)."); - ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed); + Logger.i( + TAG, + "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath)." + ); + ad.loadVideo( + if (video.isLive) "LIVE" else "BUFFERED", + "application/dash+xml", + dashUrl, + resumePosition, + video.duration.toDouble(), + speed, + metadataFromVideo(video) + ); return listOf() } - private fun deviceFromCastingDeviceInfo(deviceInfo: CastingDeviceInfo): CastingDevice { - return when (deviceInfo.type) { - CastProtocolType.CHROMECAST -> { - ChromecastCastingDevice(deviceInfo); - } - CastProtocolType.AIRPLAY -> { - AirPlayCastingDevice(deviceInfo); - } - CastProtocolType.FCAST -> { - FCastCastingDevice(deviceInfo); - } - } + fun addRememberedDevice(deviceInfo: CastingDeviceInfo): CastingDeviceInfo { + val device = deviceFromInfo(deviceInfo); + return addRememberedDevice(device); } - private fun addOrUpdateChromeCastDevice(name: String, addresses: Array, port: Int) { - return addOrUpdateCastDevice(name, - deviceFactory = { ChromecastCastingDevice(name, addresses, port) }, - deviceUpdater = { d -> - if (d.isReady) { - return@addOrUpdateCastDevice false; - } - - val changed = addresses.contentEquals(d.addresses) || d.name != name || d.port != port; - if (changed) { - d.name = name; - d.addresses = addresses; - d.port = port; - } - - return@addOrUpdateCastDevice changed; - } - ); + fun addRememberedDevice(device: CastingDevice): CastingDeviceInfo { + val deviceInfo = device.getDeviceInfo() + return _storage.addDevice(deviceInfo) } - private fun addOrUpdateAirPlayDevice(name: String, addresses: Array, port: Int) { - return addOrUpdateCastDevice(name, - deviceFactory = { AirPlayCastingDevice(name, addresses, port) }, - deviceUpdater = { d -> - if (d.isReady) { - return@addOrUpdateCastDevice false; - } - - val changed = addresses.contentEquals(addresses) || d.name != name || d.port != port; - if (changed) { - d.name = name; - d.port = port; - d.addresses = addresses; - } - - return@addOrUpdateCastDevice changed; - } - ); + fun getRememberedCastingDevices(): List { + return _storage.getDevices().map { deviceFromInfo(it) } } - private fun addOrUpdateFastCastDevice(name: String, addresses: Array, port: Int) { - return addOrUpdateCastDevice(name, - deviceFactory = { FCastCastingDevice(name, addresses, port) }, - deviceUpdater = { d -> - if (d.isReady) { - return@addOrUpdateCastDevice false; - } - - val changed = addresses.contentEquals(addresses) || d.name != name || d.port != port; - if (changed) { - d.name = name; - d.port = port; - d.addresses = addresses; - } - - return@addOrUpdateCastDevice changed; - } - ); + fun getRememberedCastingDeviceNames(): List { + return _storage.getDeviceNames() } - private inline fun addOrUpdateCastDevice(name: String, deviceFactory: () -> TCastDevice, deviceUpdater: (device: TCastDevice) -> Boolean) where TCastDevice : CastingDevice { - var invokeEvents: (() -> Unit)? = null; - - synchronized(devices) { - val device = devices[name]; - if (device != null) { - if (device !is TCastDevice) { - Logger.w(TAG, "Device name conflict between device types. Ignoring device."); - } else { - val changed = deviceUpdater(device as TCastDevice); - if (changed) { - invokeEvents = { - onDeviceChanged.emit(device); - } - } else { - - } - } - } else { - val newDevice = deviceFactory(); - this.devices[name] = newDevice; - - invokeEvents = { - onDeviceAdded.emit(newDevice); - }; - } - } - - invokeEvents?.let { _scopeMain.launch { it(); }; }; + fun removeRememberedDevice(device: CastingDevice) { + val name = device.name ?: return + _storage.removeDevice(name) } - fun enableDeveloper(enableDev: Boolean){ + fun enableDeveloper(enableDev: Boolean) { _castServer.removeAllHandlers("dev"); - if(enableDev) { + if (enableDev) { _castServer.addHandler(HttpFunctionHandler("GET", "/dashPlayer") { context -> if (context.query.containsKey("dashUrl")) { val dashUrl = context.query["dashUrl"]; - val html = "
\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
"; + val html = + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "
"; context.respondCode(200, html, "text/html"); } }).withTag("dev"); } } - @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 { - val instance: StateCasting = StateCasting(); - - private val representationRegex = Regex("(.*?)<\\/Representation>", RegexOption.DOT_MATCHES_ALL) - private val mediaInitializationRegex = Regex("(media|initiali[sz]ation)=\"([^\"]+)\"", RegexOption.DOT_MATCHES_ALL); + var instance: StateCasting = if (Settings.instance.casting.experimentalCasting) { + ExpStateCasting() + } else { + OldStateCasting() + } + private val representationRegex = Regex( + "(.*?)<\\/Representation>", + RegexOption.DOT_MATCHES_ALL + ) + private val mediaInitializationRegex = + Regex("(media|initiali[sz]ation)=\"([^\"]+)\"", RegexOption.DOT_MATCHES_ALL); private val TAG = "StateCasting"; } -} - +} \ 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 c8cdc6c4..4cc6ce80 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/CastingAddDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/CastingAddDialog.kt @@ -11,8 +11,8 @@ import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.casting.CastProtocolType +import com.futo.platformplayer.casting.OldStateCasting import com.futo.platformplayer.casting.StateCasting -import com.futo.platformplayer.experimental_casting.ExpStateCasting import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.toInetAddress import com.futo.platformplayer.logging.Logger @@ -110,14 +110,10 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) { _textError.visibility = View.GONE; val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt()); - if (Settings.instance.casting.experimentalCasting) { - try { - ExpStateCasting.instance.addRememberedDevice(castingDeviceInfo) - } catch (e: Throwable) { - Logger.e(TAG, "Failed to add remembered device: $e") - } - } else { + try { StateCasting.instance.addRememberedDevice(castingDeviceInfo) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to add remembered device: $e") } performDismiss(); }; diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt index 54dc7bc4..c6cebbe8 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt @@ -7,7 +7,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.widget.Button -import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView @@ -15,18 +14,14 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.R -import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.casting.CastConnectionState -import com.futo.platformplayer.casting.CastingDevice import com.futo.platformplayer.casting.StateCasting -import com.futo.platformplayer.experimental_casting.ExpStateCasting import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.views.adapters.DeviceAdapter import com.futo.platformplayer.views.adapters.DeviceAdapterEntry -import com.futo.platformplayer.views.adapters.GenericCastingDevice import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -58,33 +53,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) { _recyclerDevices.layoutManager = LinearLayoutManager(context); _adapter.onPin.subscribe { d -> - when (d) { - is GenericCastingDevice.Experimental -> { - val isRemembered = _rememberedDevices.contains(d.handle.device.name()) - val newIsRemembered = !isRemembered - if (newIsRemembered) { - ExpStateCasting.instance.addRememberedDevice(d.handle) - val name = d.handle.device.name() - _rememberedDevices.add(name) - } else { - ExpStateCasting.instance.removeRememberedDevice(d.handle) - _rememberedDevices.remove(d.handle.device.name()) - } - } - is GenericCastingDevice.Normal -> { - val isRemembered = _rememberedDevices.contains(d.device.name) - val newIsRemembered = !isRemembered - if (newIsRemembered) { - StateCasting.instance.addRememberedDevice(d.device) - val name = d.device.name - if (name != null) { - _rememberedDevices.add(name) - } - } else { - StateCasting.instance.removeRememberedDevice(d.device) - _rememberedDevices.remove(d.device.name) - } + val isRemembered = _rememberedDevices.contains(d.name) + val newIsRemembered = !isRemembered + if (newIsRemembered) { + StateCasting.instance.addRememberedDevice(d) + val name = d.name + if (name != null) { + _rememberedDevices.add(name) } + } else { + StateCasting.instance.removeRememberedDevice(d) + _rememberedDevices.remove(d.name) } updateUnifiedList() } @@ -124,77 +103,42 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) { (_imageLoader.drawable as Animatable?)?.start(); - if (Settings.instance.casting.experimentalCasting) { - synchronized(ExpStateCasting.instance.devices) { - _devices.addAll(ExpStateCasting.instance.devices.values.map { it.device.name() }) - } - _rememberedDevices.addAll(ExpStateCasting.instance.getRememberedCastingDeviceNames()) - } else { - synchronized(StateCasting.instance.devices) { - _devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name }) - } - _rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames()) + synchronized(StateCasting.instance.devices) { + _devices.addAll(StateCasting.instance.devices.values.map { it.name.orEmpty() }) } + _rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames()) updateUnifiedList() - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.onDeviceAdded.subscribe(this) { d -> - _devices.add(d.name()) - updateUnifiedList() - } - - ExpStateCasting.instance.onDeviceChanged.subscribe(this) { d -> - val index = _unifiedDevices.indexOfFirst { it.castingDevice.name() == d.device.name() } - if (index != -1) { - val dev = GenericCastingDevice.Experimental(d) - _unifiedDevices[index] = DeviceAdapterEntry(dev, _unifiedDevices[index].isPinnedDevice, _unifiedDevices[index].isOnlineDevice) - _adapter.notifyItemChanged(index) - } - } - - ExpStateCasting.instance.onDeviceRemoved.subscribe(this) { deviceName -> - _devices.remove(deviceName) + StateCasting.instance.onDeviceAdded.subscribe(this) { d -> + val name = d.name + if (name != null) { + _devices.add(name) updateUnifiedList() } + } - ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState -> - if (connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED) { - StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { - dismiss() - } - } - } - } else { - StateCasting.instance.onDeviceAdded.subscribe(this) { d -> - val name = d.name - if (name != null) - _devices.add(name) - updateUnifiedList() + StateCasting.instance.onDeviceChanged.subscribe(this) { d -> + val index = _unifiedDevices.indexOfFirst { it.castingDevice.name == d.name } + if (index != -1) { + _unifiedDevices[index] = DeviceAdapterEntry( + d, + _unifiedDevices[index].isPinnedDevice, + _unifiedDevices[index].isOnlineDevice + ) + _adapter.notifyItemChanged(index) } + } - StateCasting.instance.onDeviceChanged.subscribe(this) { d -> - val index = _unifiedDevices.indexOfFirst { it.castingDevice.name() == d.name } - if (index != -1) { - _unifiedDevices[index] = DeviceAdapterEntry( - GenericCastingDevice.Normal(d), - _unifiedDevices[index].isPinnedDevice, - _unifiedDevices[index].isOnlineDevice - ) - _adapter.notifyItemChanged(index) - } - } + StateCasting.instance.onDeviceRemoved.subscribe(this) { deviceName -> + _devices.remove(deviceName.name) + updateUnifiedList() + } - StateCasting.instance.onDeviceRemoved.subscribe(this) { d -> - _devices.remove(d.name) - updateUnifiedList() - } - - StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState -> - if (connectionState == CastConnectionState.CONNECTED) { - StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { - dismiss() - } + StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState -> + if (connectionState == CastConnectionState.CONNECTED) { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + dismiss() } } } @@ -203,17 +147,10 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) { override fun dismiss() { super.dismiss() (_imageLoader.drawable as Animatable?)?.stop() - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.onDeviceAdded.remove(this) - ExpStateCasting.instance.onDeviceChanged.remove(this) - ExpStateCasting.instance.onDeviceRemoved.remove(this) - ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this) - } else { - StateCasting.instance.onDeviceAdded.remove(this) - StateCasting.instance.onDeviceChanged.remove(this) - StateCasting.instance.onDeviceRemoved.remove(this) - StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this) - } + StateCasting.instance.onDeviceAdded.remove(this) + StateCasting.instance.onDeviceChanged.remove(this) + StateCasting.instance.onDeviceRemoved.remove(this) + StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this) } private fun updateUnifiedList() { @@ -226,16 +163,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldItem = oldList[oldItemPosition] val newItem = newList[newItemPosition] - return oldItem.castingDevice.name() == newItem.castingDevice.name() - && oldItem.castingDevice.isReady() == newItem.castingDevice.isReady() + return oldItem.castingDevice.name == newItem.castingDevice.name + && oldItem.castingDevice.isReady == newItem.castingDevice.isReady && oldItem.isOnlineDevice == newItem.isOnlineDevice && oldItem.isPinnedDevice == newItem.isPinnedDevice } + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldItem = oldList[oldItemPosition] val newItem = newList[newItemPosition] - return oldItem.castingDevice.name() == newItem.castingDevice.name() - && oldItem.castingDevice.isReady() == newItem.castingDevice.isReady() + return oldItem.castingDevice.name == newItem.castingDevice.name + && oldItem.castingDevice.isReady == newItem.castingDevice.isReady && oldItem.isOnlineDevice == newItem.isOnlineDevice && oldItem.isPinnedDevice == newItem.isPinnedDevice } @@ -252,64 +190,40 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) { private fun buildUnifiedList(): List { val unifiedList = mutableListOf() - if (Settings.instance.casting.experimentalCasting) { - val onlineDevices = ExpStateCasting.instance.devices.values.associateBy { it.device.name() } - val rememberedDevices = ExpStateCasting.instance.getRememberedCastingDevices().associateBy { it.device.name() } + val onlineDevices = StateCasting.instance.devices.values.associateBy { it.name } + val rememberedDevices = + StateCasting.instance.getRememberedCastingDevices().associateBy { it.name } - val intersectionNames = _devices.intersect(_rememberedDevices) - for (name in intersectionNames) { - onlineDevices[name]?.let { - unifiedList.add(DeviceAdapterEntry( - GenericCastingDevice.Experimental(it), true, true) + val intersectionNames = _devices.intersect(_rememberedDevices) + for (name in intersectionNames) { + onlineDevices[name]?.let { + unifiedList.add( + DeviceAdapterEntry( + it, true, true ) - } + ) } + } - val onlineOnlyNames = _devices - _rememberedDevices - for (name in onlineOnlyNames) { - onlineDevices[name]?.let { - unifiedList.add(DeviceAdapterEntry( - GenericCastingDevice.Experimental(it), false, true) + val onlineOnlyNames = _devices - _rememberedDevices + for (name in onlineOnlyNames) { + onlineDevices[name]?.let { + unifiedList.add( + DeviceAdapterEntry( + it, false, true ) - } + ) } + } - val rememberedOnlyNames = _rememberedDevices - _devices - for (name in rememberedOnlyNames) { - rememberedDevices[name]?.let { - unifiedList.add(DeviceAdapterEntry( - GenericCastingDevice.Experimental(it), true, false) + val rememberedOnlyNames = _rememberedDevices - _devices + for (name in rememberedOnlyNames) { + rememberedDevices[name]?.let { + unifiedList.add( + DeviceAdapterEntry( + it, true, false ) - } - } - } else { - val onlineDevices = StateCasting.instance.devices.values.associateBy { it.name } - val rememberedDevices = StateCasting.instance.getRememberedCastingDevices().associateBy { it.name } - - val intersectionNames = _devices.intersect(_rememberedDevices) - for (name in intersectionNames) { - onlineDevices[name]?.let { - unifiedList.add(DeviceAdapterEntry( - GenericCastingDevice.Normal(it), true, true) - ) - } - } - - val onlineOnlyNames = _devices - _rememberedDevices - for (name in onlineOnlyNames) { - onlineDevices[name]?.let { - unifiedList.add(DeviceAdapterEntry( - GenericCastingDevice.Normal( it), false, true)) - } - } - - val rememberedOnlyNames = _rememberedDevices - _devices - for (name in rememberedOnlyNames) { - rememberedDevices[name]?.let { - unifiedList.add(DeviceAdapterEntry( - GenericCastingDevice.Normal(it), true, false) - ) - } + ) } } 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 f9466487..bd76f3ea 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt @@ -16,16 +16,15 @@ import com.futo.platformplayer.Settings import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.casting.AirPlayCastingDevice import com.futo.platformplayer.casting.CastConnectionState +import com.futo.platformplayer.casting.CastProtocolType import com.futo.platformplayer.casting.CastingDevice import com.futo.platformplayer.casting.ChromecastCastingDevice import com.futo.platformplayer.casting.FCastCastingDevice +import com.futo.platformplayer.casting.OldStateCasting import com.futo.platformplayer.casting.StateCasting -import com.futo.platformplayer.experimental_casting.ExpStateCasting -import com.futo.platformplayer.experimental_casting.StateCastingDispatcher import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp -import com.futo.platformplayer.views.adapters.GenericCastingDevice import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider.OnChangeListener import kotlinx.coroutines.Dispatchers @@ -51,7 +50,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) { private lateinit var _buttonStop: ImageButton; private lateinit var _buttonNext: ImageButton; - private var _device: GenericCastingDevice? = null; + private var _device: CastingDevice? = null; override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState); @@ -75,18 +74,24 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) { _buttonPlay = findViewById(R.id.button_play); _buttonPlay.setOnClickListener { - StateCastingDispatcher.resumeVideo() + try { + StateCasting.instance.activeDevice?.resumePlayback() + } catch (_: Throwable) {} } _buttonPause = findViewById(R.id.button_pause); _buttonPause.setOnClickListener { - StateCastingDispatcher.pauseVideo() + try { + StateCasting.instance.activeDevice?.pausePlayback() + } catch (_: Throwable) {} } _buttonStop = findViewById(R.id.button_stop); _buttonStop.setOnClickListener { (ownerActivity as MainActivity?)?.getFragment()?.closeVideoDetails() - StateCastingDispatcher.stopVideo() + try { + StateCasting.instance.activeDevice?.stopPlayback() + } catch (_: Throwable) {} } _buttonNext = findViewById(R.id.button_next); @@ -96,16 +101,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) { _buttonClose.setOnClickListener { dismiss(); }; _buttonDisconnect.setOnClickListener { - if (Settings.instance.casting.experimentalCasting) { - try { - ExpStateCasting.instance.activeDevice?.device?.stopPlayback() - ExpStateCasting.instance.activeDevice?.device?.disconnect() - } catch (e: Throwable) { - // Ignored - } - } else { - StateCasting.instance.activeDevice?.stopCasting(); - } + try { + StateCasting.instance.activeDevice?.disconnect() + } catch (_: Throwable) {} dismiss(); }; @@ -114,7 +112,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) { return@OnChangeListener } - StateCastingDispatcher.videoSeekTo(value.toDouble()) + try { + StateCasting.instance.activeDevice?.seekTo(value.toDouble()) + } catch (_: Throwable) {} }); //TODO: Check if volume slider is properly hidden in all cases @@ -123,7 +123,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) { return@OnChangeListener } - StateCastingDispatcher.changeVolume(value.toDouble()) + try { + StateCasting.instance.activeDevice?.changeVolume(value.toDouble()) + } catch (_: Throwable) {} }); setLoading(false); @@ -134,64 +136,34 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) { super.show(); Logger.i(TAG, "Dialog shown."); - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.onActiveDeviceVolumeChanged.remove(this) - ExpStateCasting.instance.onActiveDeviceVolumeChanged.subscribe { - _sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo) - } + StateCasting.instance.onActiveDeviceVolumeChanged.remove(this) + StateCasting.instance.onActiveDeviceVolumeChanged.subscribe { + _sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo) + } - ExpStateCasting.instance.onActiveDeviceTimeChanged.remove(this) - ExpStateCasting.instance.onActiveDeviceTimeChanged.subscribe { - _sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo) - } + StateCasting.instance.onActiveDeviceTimeChanged.remove(this) + StateCasting.instance.onActiveDeviceTimeChanged.subscribe { + _sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo) + } - ExpStateCasting.instance.onActiveDeviceDurationChanged.remove(this) - ExpStateCasting.instance.onActiveDeviceDurationChanged.subscribe { - val dur = it.toFloat().coerceAtLeast(1.0f) - _sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur) - _sliderPosition.valueTo = dur - } + StateCasting.instance.onActiveDeviceDurationChanged.remove(this) + StateCasting.instance.onActiveDeviceDurationChanged.subscribe { + val dur = it.toFloat().coerceAtLeast(1.0f) + _sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur) + _sliderPosition.valueTo = dur + } - val ad = ExpStateCasting.instance.activeDevice - if (ad != null) { - _device = GenericCastingDevice.Experimental(ad) - } - val isConnected = ad != null && ad.connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED - setLoading(!isConnected) - ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState -> - StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { - setLoading(connectionState != com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED) - } - updateDevice() - } - } else { - StateCasting.instance.onActiveDeviceVolumeChanged.remove(this); - StateCasting.instance.onActiveDeviceVolumeChanged.subscribe { - _sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo); - }; - - StateCasting.instance.onActiveDeviceTimeChanged.remove(this); - StateCasting.instance.onActiveDeviceTimeChanged.subscribe { - _sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo); - }; - - StateCasting.instance.onActiveDeviceDurationChanged.remove(this); - StateCasting.instance.onActiveDeviceDurationChanged.subscribe { - val dur = it.toFloat().coerceAtLeast(1.0f) - _sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur); - _sliderPosition.valueTo = dur - }; - - val ad = StateCasting.instance.activeDevice - if (ad != null) { - _device = GenericCastingDevice.Normal(ad) - } - val isConnected = ad != null && ad.connectionState == CastConnectionState.CONNECTED; - setLoading(!isConnected); - StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState -> - StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { setLoading(connectionState != CastConnectionState.CONNECTED); }; - updateDevice() + val ad = StateCasting.instance.activeDevice + if (ad != null) { + _device = ad + } + val isConnected = ad != null && ad.connectionState == CastConnectionState.CONNECTED + setLoading(!isConnected) + StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState -> + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + setLoading(connectionState != CastConnectionState.CONNECTED) } + updateDevice() } updateDevice(); @@ -199,117 +171,71 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) { override fun dismiss() { super.dismiss(); + StateCasting.instance.onActiveDeviceVolumeChanged.remove(this); StateCasting.instance.onActiveDeviceDurationChanged.remove(this); StateCasting.instance.onActiveDeviceTimeChanged.remove(this); StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this); - ExpStateCasting.instance.onActiveDeviceVolumeChanged.remove(this); - ExpStateCasting.instance.onActiveDeviceDurationChanged.remove(this); - ExpStateCasting.instance.onActiveDeviceTimeChanged.remove(this); - ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this); _device = null; } private fun updateDevice() { - if (Settings.instance.casting.experimentalCasting) { - val d = ExpStateCasting.instance.activeDevice ?: return; + val d = StateCasting.instance.activeDevice ?: return; - when (d.device.castingProtocol()) { - ProtocolType.CHROMECAST -> { - _imageDevice.setImageResource(R.drawable.ic_chromecast); - _textType.text = "Chromecast"; - } - ProtocolType.F_CAST -> { - _imageDevice.setImageResource(R.drawable.ic_exp_fc); - _textType.text = "FCast"; - } - } - - _textName.text = d.device.name(); - _sliderPosition.valueFrom = 0.0f; - _sliderVolume.valueFrom = 0.0f; - _sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo); - - val dur = d.duration.toFloat().coerceAtLeast(1.0f) - _sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur) - _sliderPosition.valueTo = dur - - if (d.device.supportsFeature(DeviceFeature.SET_VOLUME)) { - _layoutVolumeAdjustable.visibility = View.VISIBLE; - _layoutVolumeFixed.visibility = View.GONE; - } else { - _layoutVolumeAdjustable.visibility = View.GONE; - _layoutVolumeFixed.visibility = View.VISIBLE; - } - - val interactiveControls = listOf( - _sliderPosition, - _sliderVolume, - _buttonPrevious, - _buttonPlay, - _buttonPause, - _buttonStop, - _buttonNext - ) - - when (d.connectionState) { - com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> { - enableControls(interactiveControls) - } - com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTING, - com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> { - disableControls(interactiveControls) - } - } - } else { - val d = StateCasting.instance.activeDevice ?: return; - - if (d is ChromecastCastingDevice) { + when (d.protocolType) { + CastProtocolType.CHROMECAST -> { _imageDevice.setImageResource(R.drawable.ic_chromecast); _textType.text = "Chromecast"; - } else if (d is AirPlayCastingDevice) { + } + CastProtocolType.AIRPLAY -> { _imageDevice.setImageResource(R.drawable.ic_airplay); _textType.text = "AirPlay"; - } else if (d is FCastCastingDevice) { - _imageDevice.setImageResource(R.drawable.ic_fc); + } + CastProtocolType.FCAST -> { + _imageDevice.setImageResource( + if (Settings.instance.casting.experimentalCasting) { + R.drawable.ic_exp_fc + } else { + R.drawable.ic_fc + } + ) _textType.text = "FCast"; } + } - _textName.text = d.name; - _sliderPosition.valueFrom = 0.0f; - _sliderVolume.valueFrom = 0.0f; - _sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo); + _textName.text = d.name; + _sliderPosition.valueFrom = 0.0f; + _sliderVolume.valueFrom = 0.0f; + _sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo); - val dur = d.duration.toFloat().coerceAtLeast(1.0f) - _sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur) - _sliderPosition.valueTo = dur + val dur = d.duration.toFloat().coerceAtLeast(1.0f) + _sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur) + _sliderPosition.valueTo = dur - if (d.canSetVolume) { - _layoutVolumeAdjustable.visibility = View.VISIBLE; - _layoutVolumeFixed.visibility = View.GONE; - } else { - _layoutVolumeAdjustable.visibility = View.GONE; - _layoutVolumeFixed.visibility = View.VISIBLE; + if (d.canSetVolume()) { + _layoutVolumeAdjustable.visibility = View.VISIBLE; + _layoutVolumeFixed.visibility = View.GONE; + } else { + _layoutVolumeAdjustable.visibility = View.GONE; + _layoutVolumeFixed.visibility = View.VISIBLE; + } + + val interactiveControls = listOf( + _sliderPosition, + _sliderVolume, + _buttonPrevious, + _buttonPlay, + _buttonPause, + _buttonStop, + _buttonNext + ) + + when (d.connectionState) { + CastConnectionState.CONNECTED -> { + enableControls(interactiveControls) } - - val interactiveControls = listOf( - _sliderPosition, - _sliderVolume, - _buttonPrevious, - _buttonPlay, - _buttonPause, - _buttonStop, - _buttonNext - ) - - when (d.connectionState) { - CastConnectionState.CONNECTED -> { - enableControls(interactiveControls) - } - CastConnectionState.CONNECTING, - CastConnectionState.DISCONNECTED -> { - disableControls(interactiveControls) - } + CastConnectionState.CONNECTING, CastConnectionState.DISCONNECTED -> { + disableControls(interactiveControls) } } } diff --git a/app/src/main/java/com/futo/platformplayer/experimental_casting/CastingDevice.kt b/app/src/main/java/com/futo/platformplayer/experimental_casting/CastingDevice.kt deleted file mode 100644 index fd0be55f..00000000 --- a/app/src/main/java/com/futo/platformplayer/experimental_casting/CastingDevice.kt +++ /dev/null @@ -1,181 +0,0 @@ -package com.futo.platformplayer.experimental_casting - -import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.logging.Logger -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch -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.GenericKeyEvent -import org.fcast.sender_sdk.GenericMediaEvent -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.LoadRequest -import org.fcast.sender_sdk.Metadata - -class CastingDeviceHandle { - class EventHandler : RsDeviceEventHandler { - var onConnectionStateChanged = Event1(); - var onPlayChanged = Event1() - var onTimeChanged = Event1() - var onDurationChanged = Event1() - var onVolumeChanged = Event1() - var onSpeedChanged = Event1() - - 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: GenericKeyEvent) { - // Unreachable - } - - override fun mediaEvent(event: GenericMediaEvent) { - // Unreachable - } - - override fun playbackError(message: String) { - Logger.e(TAG, "Playback error: $message") - } - } - - val eventHandler = EventHandler() - val device: RsCastingDevice - - var usedRemoteAddress: InetAddress? = null - var localAddress: InetAddress? = null - var connectionState = CastConnectionState.DISCONNECTED - var volume: Double = 1.0 - var duration: Double = 0.0 - 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; - }; - - constructor(newDevice: RsCastingDevice) { - device = newDevice - eventHandler.onConnectionStateChanged.subscribe { newState -> - if (newState == DeviceConnectionState.Disconnected) { - try { - Logger.i(TAG, "Stopping device") - device.disconnect() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to stop device: $e") - } - } - } - } - - fun loadVideo( - contentType: String, - contentId: String, - resumePosition: Double, - speed: Double?, - metadata: Metadata? = null - ) { - try { - device.load(LoadRequest.Video( - contentType = contentType, - url = contentId, - resumePosition = resumePosition, - speed = speed, - volume = volume, - metadata = metadata - )) - } catch (e: Throwable) { - Logger.e(TAG, "Failed to load video: $e") - } - } - - fun loadContent( - contentType: String, - content: String, - resumePosition: Double, - speed: Double? - ) { - try { - device.load(LoadRequest.Content( - contentType =contentType, - content = content, - resumePosition = resumePosition, - speed = speed, - volume = volume - )) - } catch (e: Throwable) { - Logger.e(TAG, "Failed to load content: $e") - } - } - - companion object { - private val TAG = "ExperimentalCastingDevice" - } -} - -enum class CastConnectionState { - DISCONNECTED, - CONNECTING, - CONNECTED -} - -@Serializable(with = ExpCastProtocolType.CastProtocolTypeSerializer::class) -enum class ExpCastProtocolType { - CHROMECAST, - FCAST; - - object CastProtocolTypeSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: ExpCastProtocolType) { - encoder.encodeString(value.name) - } - - override fun deserialize(decoder: Decoder): ExpCastProtocolType { - val name = decoder.decodeString() - return when (name) { - "FASTCAST" -> FCAST // Handle the renamed case - else -> ExpCastProtocolType.valueOf(name) - } - } - } -} diff --git a/app/src/main/java/com/futo/platformplayer/experimental_casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/experimental_casting/StateCasting.kt deleted file mode 100644 index cc4821d1..00000000 --- a/app/src/main/java/com/futo/platformplayer/experimental_casting/StateCasting.kt +++ /dev/null @@ -1,1813 +0,0 @@ -package com.futo.platformplayer.experimental_casting - -import android.app.AlertDialog -import android.content.ContentResolver -import android.content.Context -import android.os.Build -import android.os.Looper -import android.util.Log -import androidx.annotation.OptIn -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 -import com.futo.platformplayer.api.http.ManagedHttpClient -import com.futo.platformplayer.api.http.server.HttpHeaders -import com.futo.platformplayer.api.http.server.ManagedHttpServer -import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler -import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler -import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler -import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler -import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource -import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource -import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource -import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource -import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource -import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource -import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource -import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource -import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource -import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource -import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails -import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor -import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource -import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource -import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource -import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource -import com.futo.platformplayer.builders.DashBuilder -import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.constructs.Event2 -import com.futo.platformplayer.exceptions.UnsupportedCastException -import com.futo.platformplayer.findPreferredAddress -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.models.CastingDeviceInfo -import com.futo.platformplayer.parsers.HLS -import com.futo.platformplayer.states.StateApp -import com.futo.platformplayer.stores.CastingDeviceInfoStorage -import com.futo.platformplayer.stores.FragmentedStorage -import com.futo.platformplayer.toUrlAddress -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.fcast.sender_sdk.ApplicationInfo -import java.net.Inet6Address -import java.net.InetAddress -import java.net.URLDecoder -import java.net.URLEncoder -import java.util.UUID -import org.fcast.sender_sdk.DeviceInfo as RsDeviceInfo -import org.fcast.sender_sdk.CastingDevice as RsCastingDevice -import org.fcast.sender_sdk.ProtocolType -import org.fcast.sender_sdk.CastContext -import org.fcast.sender_sdk.DeviceConnectionState -import org.fcast.sender_sdk.Metadata -import org.fcast.sender_sdk.NsdDeviceDiscoverer -import org.fcast.sender_sdk.urlFormatIpAddr -import java.util.concurrent.atomic.AtomicInteger - -class ExpStateCasting { - private val _scopeIO = CoroutineScope(Dispatchers.IO) - private val _scopeMain = CoroutineScope(Dispatchers.Main) - private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get() - - private val _castServer = ManagedHttpServer() - private var _started = false - - val onDeviceAdded = Event1() - val onDeviceChanged = Event1() - val onDeviceRemoved = Event1() - val onActiveDeviceTimeChanged = Event1() - val onActiveDeviceDurationChanged = Event1() - val onActiveDeviceVolumeChanged = Event1() - val onActiveDevicePlayChanged = Event1() - val onActiveDeviceConnectionStateChanged = Event2() - private var _videoExecutor: JSRequestExecutor? = null - private var _audioExecutor: JSRequestExecutor? = null - private val _client = ManagedHttpClient() - val isCasting: Boolean get() = activeDevice != null - private val _context = CastContext() - var activeDevice: CastingDeviceHandle? = null - var devices: HashMap = hashMapOf() - var _resumeCastingDevice: RsDeviceInfo? = null - var _deviceDiscoverer: NsdDeviceDiscoverer? = null - private val _castId = AtomicInteger(0) - - 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(context: Context, url: String) { - try { - val foundDeviceInfo = org.fcast.sender_sdk.deviceInfoFromUrl(url)!! - val foundDevice = _context.createDeviceFromInfo(foundDeviceInfo) - connectDevice(CastingDeviceHandle(foundDevice)) - } catch (e: Throwable) { - Logger.e(TAG, "Failed to handle URL: $e") - } - } - - fun onStop() { - val ad = activeDevice ?: return - _resumeCastingDevice = ad.device.getDeviceInfo() - Log.i(TAG, "_resumeCastingDevice set to '${ad.device.name()}'") - Logger.i(TAG, "Stopping active device because of onStop.") - try { - ad.device.disconnect() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to disconnect from device: $e") - } - } - - fun onResume() { - val ad = activeDevice - if (ad != null) { - // TODO: needed? - // if (ad is FCastCastingDevice) { - // ad.ensureThreadStarted() - // } else if (ad is ChromecastCastingDevice) { - // ad.ensureThreadsStarted() - // } - } else { - val resumeCastingDevice = _resumeCastingDevice - if (resumeCastingDevice != null) { - try { - connectDevice( - CastingDeviceHandle(_context.createDeviceFromInfo(resumeCastingDevice)) - ) - _resumeCastingDevice = null - Log.i(TAG, "_resumeCastingDevice set to null onResume") - } catch (e: Throwable) { - Logger.e(TAG, "Failed to resume: $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 = CastingDeviceHandle(device) - devices[deviceHandle.device.name()] = deviceHandle - invokeInMainScopeIfRequired { - onDeviceAdded.emit(device) - } - }, - { deviceName -> // Removed - invokeInMainScopeIfRequired { - if (devices.containsKey(deviceName)) { - devices.remove(deviceName) - } - onDeviceRemoved.emit(deviceName) - } - }, - { deviceInfo -> // Updated - Logger.i(TAG, "Device updated: $deviceInfo") - val handle = devices[deviceInfo.name] - if (handle != null) { - 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; - d?.device?.disconnect(); - - _castServer.stop(); - _castServer.removeAllHandlers(); - - Logger.i(TAG, "CastingService stopped.") - - _deviceDiscoverer = null - } - - private val _castingDialogLock = Any(); - private var _currentDialog: AlertDialog? = null; - - @Synchronized - fun connectDevice(device: CastingDeviceHandle) { - if (activeDevice == device) - return; - - val ad = activeDevice; - if (ad != null) { - Logger.i(TAG, "Stopping previous device because a new one is being connected.") - device.eventHandler.onConnectionStateChanged.clear(); - device.eventHandler.onPlayChanged.clear(); - device.eventHandler.onTimeChanged.clear(); - device.eventHandler.onVolumeChanged.clear(); - device.eventHandler.onDurationChanged.clear(); - try { - ad.device.disconnect(); - } catch (e: Throwable) { - Logger.e(TAG, "Failed to disconnect from device: $e") - } - } - - device.eventHandler.onConnectionStateChanged.subscribe { castConnectionState -> - Logger.i(TAG, "Active device connection state changed: $castConnectionState"); - - if (castConnectionState == DeviceConnectionState.Disconnected) { - Logger.i(TAG, "Clearing events: $castConnectionState"); - device.eventHandler.onConnectionStateChanged.clear(); - device.eventHandler.onPlayChanged.clear(); - device.eventHandler.onTimeChanged.clear(); - device.eventHandler.onVolumeChanged.clear(); - device.eventHandler.onDurationChanged.clear(); - activeDevice = null; - } - - invokeInMainScopeIfRequired { - StateApp.withContext(false) { context -> - context.let { - Logger.i(TAG, "Casting state changed to ${castConnectionState}"); - when (castConnectionState) { - is DeviceConnectionState.Connected -> { - device.connectionState = CastConnectionState.CONNECTED - val localAddrOctets = - org.fcast.sender_sdk.octetsFromIpAddr(castConnectionState.localAddr) - val remoteAddrOctets = - org.fcast.sender_sdk.octetsFromIpAddr(castConnectionState.usedRemoteAddr) - device.localAddress = InetAddress.getByAddress(localAddrOctets) - device.usedRemoteAddress = - InetAddress.getByAddress(remoteAddrOctets) - Logger.i(TAG, "Casting connected to [${device.device.name()}]"); - UIDialogs.appToast("Connected to device") - synchronized(_castingDialogLock) { - if (_currentDialog != null) { - _currentDialog?.hide(); - _currentDialog = null; - } - } - onActiveDeviceConnectionStateChanged.emit( - device, - CastConnectionState.CONNECTED - ); - } - - DeviceConnectionState.Connecting, DeviceConnectionState.Reconnecting -> { - synchronized(_castingDialogLock) { - if (_currentDialog == null) { - device.connectionState = CastConnectionState.CONNECTING - Logger.i(TAG, "Casting connecting to [${device.device.name()}]"); - UIDialogs.toast(it, "Connecting to device...") - _currentDialog = UIDialogs.showDialog( - context, - R.drawable.ic_loader_animated, - true, - "Connecting to [${device.device.name()}]", - "Make sure you are on the same network\n\nVPNs and guest networks can cause issues", - null, - -2, - UIDialogs.Action("Disconnect", { - try { - device.device.disconnect(); - } catch (e: Throwable) { - Logger.e(TAG, "Failed to stop device: $e") - } - }) - ); - } - } - onActiveDeviceConnectionStateChanged.emit( - device, - CastConnectionState.CONNECTING - ); - } - - DeviceConnectionState.Disconnected -> { - device.connectionState = CastConnectionState.DISCONNECTED - UIDialogs.toast(it, "Disconnected from device") - synchronized(_castingDialogLock) { - if (_currentDialog != null) { - _currentDialog?.hide(); - _currentDialog = null; - } - } - onActiveDeviceConnectionStateChanged.emit( - device, - CastConnectionState.DISCONNECTED - ); - } - } - } - }; - }; - }; - - device.eventHandler.onPlayChanged.subscribe { - invokeInMainScopeIfRequired { - device.isPlaying = it - onActiveDevicePlayChanged.emit(it) - } - } - device.eventHandler.onDurationChanged.subscribe { - invokeInMainScopeIfRequired { - device.duration = it - onActiveDeviceDurationChanged.emit(it) - } - } - device.eventHandler.onVolumeChanged.subscribe { - invokeInMainScopeIfRequired { - device.volume = it - onActiveDeviceVolumeChanged.emit(it) - } - } - device.eventHandler.onTimeChanged.subscribe { - invokeInMainScopeIfRequired { - device.time = it - device.lastTimeChangeTime_ms = System.currentTimeMillis() - onActiveDeviceTimeChanged.emit(it) - } - } - device.eventHandler.onSpeedChanged.subscribe { - invokeInMainScopeIfRequired { - device.speed = it - } - } - - try { - device.device.connect( - ApplicationInfo( - "Grayjay Android", - "${BuildConfig.VERSION_NAME}-${BuildConfig.FLAVOR}", - "${Build.MANUFACTURER} ${Build.MODEL}" - ), - device.eventHandler, - 1000.toULong() - ) - Logger.i(TAG, "Requested manager to start device") - } catch (e: Throwable) { - Logger.w(TAG, "Failed to connect to device."); - device.eventHandler.onConnectionStateChanged.clear(); - device.eventHandler.onPlayChanged.clear(); - device.eventHandler.onTimeChanged.clear(); - device.eventHandler.onVolumeChanged.clear(); - device.eventHandler.onDurationChanged.clear(); - return; - } - - activeDevice = device; - Logger.i(TAG, "Started device `${device.device.name()}`"); - } - - fun addRememberedDevice(deviceInfo: CastingDeviceInfo): CastingDeviceInfo { - val device = deviceFromCastingDeviceInfo(deviceInfo); - return addRememberedDevice(device); - } - - fun getRememberedCastingDevices(): List { - return _storage.getDevices().map { deviceFromCastingDeviceInfo(it) } - } - - fun getRememberedCastingDeviceNames(): List { - return _storage.getDeviceNames() - } - - fun addRememberedDevice(device: CastingDeviceHandle): CastingDeviceInfo { - val rsDeviceInfo = device.device.getDeviceInfo() - val deviceInfo = CastingDeviceInfo( - name = device.device.name(), - type = when (rsDeviceInfo.protocol) { - ProtocolType.CHROMECAST -> com.futo.platformplayer.casting.CastProtocolType.CHROMECAST - ProtocolType.F_CAST -> com.futo.platformplayer.casting.CastProtocolType.FCAST - }, - addresses = rsDeviceInfo.addresses.map { urlFormatIpAddr(it) }.toTypedArray(), - port = rsDeviceInfo.port.toInt(), - ) - return _storage.addDevice(deviceInfo) - } - - fun removeRememberedDevice(device: CastingDeviceHandle) { - val name = device.device.name() - _storage.removeDevice(name) - } - - private fun invokeInMainScopeIfRequired(action: () -> Unit) { - if (Looper.getMainLooper().thread != Thread.currentThread()) { - _scopeMain.launch { action(); } - return; - } - - action(); - } - - private fun shouldProxyStreams( - deviceHandle: CastingDeviceHandle, - videoSource: IVideoSource?, - audioSource: IAudioSource? - ): Boolean { - val hasRequestModifier = - (videoSource as? JSSource)?.hasRequestModifier == true || (audioSource as? JSSource)?.hasRequestModifier == true - return Settings.instance.casting.alwaysProxyRequests || deviceHandle.device.castingProtocol() != ProtocolType.F_CAST || hasRequestModifier - } - - fun cancel() { - _castId.incrementAndGet() - } - - private fun metadataFromVideo(video: IPlatformVideoDetails): Metadata { - return Metadata( - title = video.name, - thumbnailUrl = video.thumbnails.getHQThumbnail() - ) - } - - suspend fun castIfAvailable( - contentResolver: ContentResolver, - video: IPlatformVideoDetails, - videoSource: IVideoSource?, - audioSource: IAudioSource?, - subtitleSource: ISubtitleSource?, - ms: Long = -1, - speed: Double?, - onLoadingEstimate: ((Int) -> Unit)? = null, - onLoading: ((Boolean) -> Unit)? = null - ): Boolean { - return withContext(Dispatchers.IO) { - val ad = activeDevice ?: return@withContext false; - if (ad.connectionState != CastConnectionState.CONNECTED) { - return@withContext false; - } - - val resumePosition = if (ms > 0L) (ms.toDouble() / 1000.0) else 0.0; - val castId = _castId.incrementAndGet() - - var sourceCount = 0; - if (videoSource != null) sourceCount++; - if (audioSource != null) sourceCount++; - if (subtitleSource != null) sourceCount++; - - if (sourceCount < 1) { - throw Exception("At least one source should be specified."); - } - - if (sourceCount > 1) { - if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) { - Logger.i(TAG, "Casting as local DASH"); - castLocalDash( - video, - videoSource as LocalVideoSource?, - audioSource as LocalAudioSource?, - subtitleSource as LocalSubtitleSource?, - resumePosition, - speed - ); - } else { - val isRawDash = - videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource - if (isRawDash) { - Logger.i(TAG, "Casting as raw DASH"); - - castDashRaw( - contentResolver, - video, - videoSource as JSDashManifestRawSource?, - audioSource as JSDashManifestRawAudioSource?, - subtitleSource, - resumePosition, - speed, - castId, - onLoadingEstimate, - onLoading - ); - } else { - if (ad.device.castingProtocol() == ProtocolType.F_CAST) { - Logger.i(TAG, "Casting as DASH direct"); - castDashDirect( - contentResolver, - video, - videoSource as IVideoUrlSource?, - audioSource as IAudioUrlSource?, - subtitleSource, - resumePosition, - speed - ); - } else { - Logger.i(TAG, "Casting as DASH indirect"); - castDashIndirect( - contentResolver, - video, - videoSource as IVideoUrlSource?, - audioSource as IAudioUrlSource?, - subtitleSource, - resumePosition, - speed - ); - } - } - } - } else { - val proxyStreams = shouldProxyStreams(ad, videoSource, audioSource) - val url = getLocalUrl(ad); - val id = UUID.randomUUID(); - - if (videoSource is IVideoUrlSource) { - val videoPath = "/video-${id}" - val videoUrl = if (proxyStreams) url + videoPath else videoSource.getVideoUrl(); - Logger.i(TAG, "Casting as singular video"); - ad.loadVideo( - videoSource.container, - videoUrl, - resumePosition, - speed, - metadataFromVideo(video) - ) - } else if (audioSource is IAudioUrlSource) { - val audioPath = "/audio-${id}" - val audioUrl = if (proxyStreams) url + audioPath else audioSource.getAudioUrl(); - Logger.i(TAG, "Casting as singular audio"); - ad.loadVideo( - audioSource.container, - audioUrl, - resumePosition, - speed, - metadataFromVideo(video) - ); - } else if (videoSource is IHLSManifestSource) { - if (proxyStreams || ad.device.castingProtocol() == ProtocolType.CHROMECAST) { - Logger.i(TAG, "Casting as proxied HLS"); - castProxiedHls( - video, - videoSource.url, - videoSource.codec, - resumePosition, - speed - ); - } else { - Logger.i(TAG, "Casting as non-proxied HLS"); - ad.loadVideo( - videoSource.container, - videoSource.url, - resumePosition, - speed, - metadataFromVideo(video) - ); - } - } else if (audioSource is IHLSManifestAudioSource) { - if (proxyStreams || ad.device.castingProtocol() == ProtocolType.CHROMECAST) { - Logger.i(TAG, "Casting as proxied audio HLS"); - castProxiedHls( - video, - audioSource.url, - audioSource.codec, - resumePosition, - speed - ); - } else { - Logger.i(TAG, "Casting as non-proxied audio HLS"); - ad.loadVideo( - audioSource.container, - audioSource.url, - resumePosition, - speed, - metadataFromVideo(video) - ); - } - } else if (videoSource is LocalVideoSource) { - Logger.i(TAG, "Casting as local video"); - castLocalVideo(video, videoSource, resumePosition, speed); - } else if (audioSource is LocalAudioSource) { - Logger.i(TAG, "Casting as local audio"); - castLocalAudio(video, audioSource, resumePosition, speed); - } else if (videoSource is JSDashManifestRawSource) { - Logger.i(TAG, "Casting as JSDashManifestRawSource video"); - castDashRaw( - contentResolver, - video, - videoSource as JSDashManifestRawSource?, - null, - null, - resumePosition, - speed, - castId, - onLoadingEstimate, - onLoading - ); - } else if (audioSource is JSDashManifestRawAudioSource) { - Logger.i(TAG, "Casting as JSDashManifestRawSource audio"); - castDashRaw( - contentResolver, - video, - null, - audioSource as JSDashManifestRawAudioSource?, - null, - resumePosition, - speed, - castId, - onLoadingEstimate, - onLoading - ); - } else { - var str = listOf( - if (videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null, - if (audioSource != null) "Audio: ${audioSource::class.java.simpleName}" else null, - if (subtitleSource != null) "Subtitles: ${subtitleSource::class.java.simpleName}" else null - ).filterNotNull().joinToString(", "); - throw UnsupportedCastException(str); - } - } - - return@withContext true; - } - } - - fun resumeVideo(): Boolean { - val ad = activeDevice ?: return false; - try { - ad.device.resumePlayback() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to resume playback: $e") - } - return true; - } - - fun pauseVideo(): Boolean { - val ad = activeDevice ?: return false; - try { - ad.device.pausePlayback() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to pause playback: $e") - } - return true; - } - - fun stopVideo(): Boolean { - val ad = activeDevice ?: return false; - try { - ad.device.stopPlayback() - } catch (e: Throwable) { - Logger.e(TAG, "Failed to stop playback: $e") - } - return true; - } - - fun videoSeekTo(timeSeconds: Double): Boolean { - val ad = activeDevice ?: return false; - try { - ad.device.seek(timeSeconds) - } catch (e: Throwable) { - Logger.e(TAG, "Failed to seek: $e") - } - return true; - } - - private fun castLocalVideo( - video: IPlatformVideoDetails, - videoSource: LocalVideoSource, - resumePosition: Double, - speed: Double? - ): List { - val ad = activeDevice ?: return listOf(); - - val url = getLocalUrl(ad); - val id = UUID.randomUUID(); - val videoPath = "/video-${id}" - val videoUrl = url + videoPath; - - _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - - Logger.i(TAG, "Casting local video (videoUrl: $videoUrl)."); - ad.loadVideo( - videoSource.container, - videoUrl, - resumePosition, - speed, - metadataFromVideo(video) - ); - - return listOf(videoUrl); - } - - private fun castLocalAudio( - video: IPlatformVideoDetails, - audioSource: LocalAudioSource, - resumePosition: Double, - speed: Double? - ): List { - val ad = activeDevice ?: return listOf(); - - val url = getLocalUrl(ad); - val id = UUID.randomUUID(); - val audioPath = "/audio-${id}" - val audioUrl = url + audioPath; - - _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - - Logger.i(TAG, "Casting local audio (audioUrl: $audioUrl)."); - ad.loadVideo( - audioSource.container, - audioUrl, - resumePosition, - speed, - metadataFromVideo(video) - ); - - return listOf(audioUrl); - } - - private fun castLocalHls( - video: IPlatformVideoDetails, - videoSource: LocalVideoSource?, - audioSource: LocalAudioSource?, - subtitleSource: LocalSubtitleSource?, - resumePosition: Double, - speed: Double? - ): List { - val ad = activeDevice ?: return listOf() - - val url = getLocalUrl(ad) - val id = UUID.randomUUID() - - val hlsPath = "/hls-${id}" - val videoPath = "/video-${id}" - val audioPath = "/audio-${id}" - val subtitlePath = "/subtitle-${id}" - - val hlsUrl = url + hlsPath - val videoUrl = url + videoPath - val audioUrl = url + audioPath - val subtitleUrl = url + subtitlePath - - val mediaRenditions = arrayListOf() - val variantPlaylistReferences = arrayListOf() - - if (videoSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castLocalHls") - - val duration = videoSource.duration - val videoVariantPlaylistPath = "/video-playlist-${id}" - val videoVariantPlaylistUrl = url + videoVariantPlaylistPath - val videoVariantPlaylistSegments = - listOf(HLS.MediaSegment(duration.toDouble(), videoUrl)) - val videoVariantPlaylist = HLS.VariantPlaylist( - 3, - duration.toInt(), - 0, - 0, - null, - null, - null, - videoVariantPlaylistSegments - ) - - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler( - "GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl" - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castLocalHls") - - variantPlaylistReferences.add( - HLS.VariantPlaylistReference( - videoVariantPlaylistUrl, HLS.StreamInfo( - videoSource.bitrate, - "${videoSource.width}x${videoSource.height}", - videoSource.codec, - null, - null, - if (audioSource != null) "audio" else null, - if (subtitleSource != null) "subtitles" else null, - null, - null - ) - ) - ) - } - - if (audioSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castLocalHls") - - val duration = - audioSource.duration ?: videoSource?.duration ?: throw Exception("Duration unknown") - val audioVariantPlaylistPath = "/audio-playlist-${id}" - val audioVariantPlaylistUrl = url + audioVariantPlaylistPath - val audioVariantPlaylistSegments = - listOf(HLS.MediaSegment(duration.toDouble(), audioUrl)) - val audioVariantPlaylist = HLS.VariantPlaylist( - 3, - duration.toInt(), - 0, - 0, - null, - null, - null, - audioVariantPlaylistSegments - ) - - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler( - "GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl" - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castLocalHls") - - mediaRenditions.add( - HLS.MediaRendition( - "AUDIO", - audioVariantPlaylistUrl, - "audio", - "df", - "default", - true, - true, - true - ) - ) - } - - if (subtitleSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler( - "GET", - subtitlePath, - subtitleSource.format ?: "text/vtt", - subtitleSource.filePath - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castLocalHls") - - val duration = videoSource?.duration ?: audioSource?.duration - ?: throw Exception("Duration unknown") - val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}" - val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath - val subtitleVariantPlaylistSegments = - listOf(HLS.MediaSegment(duration.toDouble(), subtitleUrl)) - val subtitleVariantPlaylist = HLS.VariantPlaylist( - 3, - duration.toInt(), - 0, - 0, - null, - null, - null, - subtitleVariantPlaylistSegments - ) - - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler( - "GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl" - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castLocalHls") - - mediaRenditions.add( - HLS.MediaRendition( - "SUBTITLES", - subtitleVariantPlaylistUrl, - "subtitles", - "df", - "default", - true, - true, - true - ) - ) - } - - val masterPlaylist = - HLS.MasterPlaylist(variantPlaylistReferences, mediaRenditions, listOf(), true) - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler( - "GET", hlsPath, masterPlaylist.buildM3U8(), - "application/vnd.apple.mpegurl" - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castLocalHls") - - Logger.i( - TAG, - "added new castLocalHls handlers (hlsPath: $hlsPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath)." - ) - ad.loadVideo( - "application/vnd.apple.mpegurl", - hlsUrl, - resumePosition, - speed, - metadataFromVideo(video) - ) - - return listOf(hlsUrl, videoUrl, audioUrl, subtitleUrl) - } - - private fun castLocalDash( - video: IPlatformVideoDetails, - videoSource: LocalVideoSource?, - audioSource: LocalAudioSource?, - subtitleSource: LocalSubtitleSource?, - resumePosition: Double, - speed: Double? - ): List { - val ad = activeDevice ?: return listOf(); - - val url = getLocalUrl(ad); - val id = UUID.randomUUID(); - - val dashPath = "/dash-${id}" - val videoPath = "/video-${id}" - val audioPath = "/audio-${id}" - val subtitlePath = "/subtitle-${id}" - - val dashUrl = url + dashPath; - val videoUrl = url + videoPath; - val audioUrl = url + audioPath; - val subtitleUrl = url + subtitlePath; - - val dashContent = DashBuilder.generateOnDemandDash( - videoSource, - videoUrl, - audioSource, - audioUrl, - subtitleSource, - subtitleUrl - ); - Logger.v(TAG) { "Dash manifest: $dashContent" }; - - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler( - "GET", dashPath, dashContent, - "application/dash+xml" - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - if (videoSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", videoPath, videoSource.container, videoSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - } - if (audioSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler("GET", audioPath, audioSource.container, audioSource.filePath) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - } - if (subtitleSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpFileHandler( - "GET", - subtitlePath, - subtitleSource.format ?: "text/vtt", - subtitleSource.filePath - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - } - - Logger.i( - TAG, - "added new castLocalDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath, subtitlePath: $subtitlePath)." - ); - ad.loadVideo( - "application/dash+xml", - dashUrl, - resumePosition, - speed, - metadataFromVideo(video) - ); - - return listOf(dashUrl, videoUrl, audioUrl, subtitleUrl); - } - - private suspend fun castDashDirect( - contentResolver: ContentResolver, - video: IPlatformVideoDetails, - videoSource: IVideoUrlSource?, - audioSource: IAudioUrlSource?, - subtitleSource: ISubtitleSource?, - resumePosition: Double, - speed: Double? - ): List { - val ad = activeDevice ?: return listOf(); - val proxyStreams = - Settings.instance.casting.alwaysProxyRequests || ad.device.castingProtocol() != ProtocolType.F_CAST - - val url = getLocalUrl(ad); - val id = UUID.randomUUID(); - - val videoPath = "/video-${id}" - val audioPath = "/audio-${id}" - val subtitlePath = "/subtitle-${id}" - - val videoUrl = if (proxyStreams) url + videoPath else videoSource?.getVideoUrl(); - val audioUrl = if (proxyStreams) url + audioPath else audioSource?.getAudioUrl(); - - val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) { - return@withContext subtitleSource.getSubtitlesURI(); - } else null; - - var subtitlesUrl: String? = null; - if (subtitlesUri != null) { - if (subtitlesUri.scheme == "file") { - var content: String? = null; - val inputStream = contentResolver.openInputStream(subtitlesUri); - inputStream?.use { stream -> - val reader = stream.bufferedReader(); - content = reader.use { it.readText() }; - } - - if (content != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler( - "GET", - subtitlePath, - content!!, - subtitleSource?.format ?: "text/vtt" - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - } - - subtitlesUrl = url + subtitlePath; - } else { - subtitlesUrl = subtitlesUri.toString(); - } - } - - if (videoSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - } - if (audioSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - } - - val content = DashBuilder.generateOnDemandDash( - videoSource, - videoUrl, - audioSource, - audioUrl, - subtitleSource, - subtitlesUrl - ); - - Logger.i( - TAG, - "Direct dash cast to casting device (videoUrl: $videoUrl, audioUrl: $audioUrl)." - ); - Logger.v(TAG) { "Dash manifest: $content" }; - ad.loadContent( - "application/dash+xml", - content, - resumePosition, - speed - ); - - return listOf( - videoUrl ?: "", - audioUrl ?: "", - subtitlesUrl ?: "", - videoSource?.getVideoUrl() ?: "", - audioSource?.getAudioUrl() ?: "", - subtitlesUri.toString() - ); - } - - private fun castProxiedHls( - video: IPlatformVideoDetails, - sourceUrl: String, - codec: String?, - resumePosition: Double, - speed: Double? - ): List { - _castServer.removeAllHandlers("castProxiedHlsMaster") - - val ad = activeDevice ?: return listOf(); - val url = getLocalUrl(ad); - - val id = UUID.randomUUID(); - val hlsPath = "/hls-${id}" - val hlsUrl = url + hlsPath - Logger.i(TAG, "HLS url: $hlsUrl"); - - _castServer.addHandlerWithAllowAllOptions( - HttpFunctionHandler( - "GET", - hlsPath - ) { masterContext -> - _castServer.removeAllHandlers("castProxiedHlsVariant") - - val headers = masterContext.headers.clone() - headers["Content-Type"] = "application/vnd.apple.mpegurl"; - - val masterPlaylistResponse = _client.get(sourceUrl) - check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" } - - val masterPlaylistContent = masterPlaylistResponse.body?.string() - ?: throw Exception("Master playlist content is empty") - - val masterPlaylist: HLS.MasterPlaylist - try { - masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl) - } catch (e: Throwable) { - if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) { - //This is a variant playlist, not a master playlist - Logger.i(TAG, "HLS casting as variant playlist (codec: $codec): $hlsUrl"); - - val vpHeaders = masterContext.headers.clone() - vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; - - val variantPlaylist = - HLS.parseVariantPlaylist(masterPlaylistContent, sourceUrl) - val proxiedVariantPlaylist = - proxyVariantPlaylist(url, id, variantPlaylist, video.isLive) - val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() - masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); - return@HttpFunctionHandler - } else { - throw e - } - } - - Logger.i(TAG, "HLS casting as master playlist: $hlsUrl"); - - val newVariantPlaylistRefs = arrayListOf() - val newMediaRenditions = arrayListOf() - val newMasterPlaylist = HLS.MasterPlaylist( - newVariantPlaylistRefs, - newMediaRenditions, - masterPlaylist.sessionDataList, - masterPlaylist.independentSegments - ) - - for (variantPlaylistRef in masterPlaylist.variantPlaylistsRefs) { - val playlistId = UUID.randomUUID(); - val newPlaylistPath = "/hls-playlist-${playlistId}" - val newPlaylistUrl = url + newPlaylistPath; - - _castServer.addHandlerWithAllowAllOptions( - HttpFunctionHandler( - "GET", - newPlaylistPath - ) { vpContext -> - val vpHeaders = vpContext.headers.clone() - vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; - - val response = _client.get(variantPlaylistRef.url) - check(response.isOk) { "Failed to get variant playlist: ${response.code}" } - - val vpContent = response.body?.string() - ?: throw Exception("Variant playlist content is empty") - - val variantPlaylist = - HLS.parseVariantPlaylist(vpContent, variantPlaylistRef.url) - val proxiedVariantPlaylist = - proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive) - val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() - vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); - }.withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castProxiedHlsVariant") - - newVariantPlaylistRefs.add( - HLS.VariantPlaylistReference( - newPlaylistUrl, - variantPlaylistRef.streamInfo - ) - ) - } - - for (mediaRendition in masterPlaylist.mediaRenditions) { - val playlistId = UUID.randomUUID() - - var newPlaylistUrl: String? = null - if (mediaRendition.uri != null) { - val newPlaylistPath = "/hls-playlist-${playlistId}" - newPlaylistUrl = url + newPlaylistPath - - _castServer.addHandlerWithAllowAllOptions( - HttpFunctionHandler( - "GET", - newPlaylistPath - ) { vpContext -> - val vpHeaders = vpContext.headers.clone() - vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; - - val response = _client.get(mediaRendition.uri) - check(response.isOk) { "Failed to get variant playlist: ${response.code}" } - - val vpContent = response.body?.string() - ?: throw Exception("Variant playlist content is empty") - - val variantPlaylist = - HLS.parseVariantPlaylist(vpContent, mediaRendition.uri) - val proxiedVariantPlaylist = proxyVariantPlaylist( - url, - playlistId, - variantPlaylist, - video.isLive - ) - val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() - vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); - }.withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castProxiedHlsVariant") - } - - newMediaRenditions.add( - HLS.MediaRendition( - mediaRendition.type, - newPlaylistUrl, - mediaRendition.groupID, - mediaRendition.language, - mediaRendition.name, - mediaRendition.isDefault, - mediaRendition.isAutoSelect, - mediaRendition.isForced - ) - ) - } - - masterContext.respondCode(200, headers, newMasterPlaylist.buildM3U8()); - }.withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castProxiedHlsMaster") - - Logger.i(TAG, "added new castHlsIndirect handlers (hlsPath: $hlsPath)."); - - //ChromeCast is sometimes funky with resume position 0 - val hackfixResumePosition = - if (ad.device.castingProtocol() == ProtocolType.CHROMECAST && !video.isLive && resumePosition == 0.0) 0.1 else resumePosition; - ad.loadVideo( - "application/vnd.apple.mpegurl", - hlsUrl, - hackfixResumePosition, - speed, - metadataFromVideo(video) - ); - - return listOf(hlsUrl); - } - - private fun proxyVariantPlaylist( - url: String, - playlistId: UUID, - variantPlaylist: HLS.VariantPlaylist, - isLive: Boolean, - proxySegments: Boolean = true - ): HLS.VariantPlaylist { - val newSegments = arrayListOf() - - if (proxySegments) { - variantPlaylist.segments.forEachIndexed { index, segment -> - val sequenceNumber = (variantPlaylist.mediaSequence ?: 0) + index.toLong() - newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber)) - } - } else { - newSegments.addAll(variantPlaylist.segments) - } - - return HLS.VariantPlaylist( - variantPlaylist.version, - variantPlaylist.targetDuration, - variantPlaylist.mediaSequence, - variantPlaylist.discontinuitySequence, - variantPlaylist.programDateTime, - variantPlaylist.playlistType, - variantPlaylist.streamInfo, - newSegments - ) - } - - private fun proxySegment( - url: String, - playlistId: UUID, - segment: HLS.Segment, - index: Long - ): HLS.Segment { - if (segment is HLS.MediaSegment) { - val newSegmentPath = "/hls-playlist-${playlistId}-segment-${index}" - val newSegmentUrl = url + newSegmentPath; - - if (_castServer.getHandler("GET", newSegmentPath) == null) { - _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", newSegmentPath, segment.uri, true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castProxiedHlsVariant") - } - - return HLS.MediaSegment( - segment.duration, - newSegmentUrl - ) - } else { - return segment - } - } - - private suspend fun castDashIndirect( - contentResolver: ContentResolver, - video: IPlatformVideoDetails, - videoSource: IVideoUrlSource?, - audioSource: IAudioUrlSource?, - subtitleSource: ISubtitleSource?, - resumePosition: Double, - speed: Double? - ): List { - val ad = activeDevice ?: return listOf(); - val proxyStreams = - Settings.instance.casting.alwaysProxyRequests || ad.device.castingProtocol() != ProtocolType.F_CAST - - val url = getLocalUrl(ad); - val id = UUID.randomUUID(); - - val dashPath = "/dash-${id}" - val videoPath = "/video-${id}" - val audioPath = "/audio-${id}" - val subtitlePath = "/subtitle-${id}" - - val dashUrl = url + dashPath; - Logger.i(TAG, "DASH url: $dashUrl"); - - val videoUrl = if (proxyStreams) url + videoPath else videoSource?.getVideoUrl(); - val audioUrl = if (proxyStreams) url + audioPath else audioSource?.getAudioUrl(); - - val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) { - return@withContext subtitleSource.getSubtitlesURI(); - } else null; - - //_castServer.removeAllHandlers("cast"); - //Logger.i(TAG, "removed all old castDash handlers."); - - var subtitlesUrl: String? = null; - if (subtitlesUri != null) { - if (subtitlesUri.scheme == "file") { - var content: String? = null; - val inputStream = contentResolver.openInputStream(subtitlesUri); - inputStream?.use { stream -> - val reader = stream.bufferedReader(); - content = reader.use { it.readText() }; - } - - if (content != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler( - "GET", - subtitlePath, - content!!, - subtitleSource?.format ?: "text/vtt" - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - } - - subtitlesUrl = url + subtitlePath; - } else { - subtitlesUrl = subtitlesUri.toString(); - } - } - - val dashContent = DashBuilder.generateOnDemandDash( - videoSource, - videoUrl, - audioSource, - audioUrl, - subtitleSource, - subtitlesUrl - ); - Logger.v(TAG) { "Dash manifest: $dashContent" }; - - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler( - "GET", dashPath, dashContent, - "application/dash+xml" - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - - if (videoSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - } - if (audioSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpProxyHandler("GET", audioPath, audioSource.getAudioUrl(), true) - .withInjectedHost() - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - } - - Logger.i( - TAG, - "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath)." - ); - ad.loadVideo( - "application/dash+xml", - dashUrl, - resumePosition, - speed, - metadataFromVideo(video) - ); - - return listOf( - dashUrl, - videoUrl ?: "", - audioUrl ?: "", - subtitlesUrl ?: "", - videoSource?.getVideoUrl() ?: "", - audioSource?.getAudioUrl() ?: "", - subtitlesUri.toString() - ); - } - - private fun cleanExecutors() { - if (_videoExecutor != null) { - _videoExecutor?.cleanup() - _videoExecutor = null - } - - if (_audioExecutor != null) { - _audioExecutor?.cleanup() - _audioExecutor = null - } - } - - private fun getLocalUrl(ad: CastingDeviceHandle): String { - var address = ad.localAddress!! - if (Settings.instance.casting.allowLinkLocalIpv4) { - if (address.isLinkLocalAddress && address is Inet6Address) { - address = findPreferredAddress() ?: address - Logger.i(TAG, "Selected casting address: $address") - } - } else { - if (address.isLinkLocalAddress) { - address = findPreferredAddress() ?: address - Logger.i(TAG, "Selected casting address: $address") - } - } - return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}"; - } - - @OptIn(UnstableApi::class) - private suspend fun castDashRaw( - contentResolver: ContentResolver, - video: IPlatformVideoDetails, - videoSource: JSDashManifestRawSource?, - audioSource: JSDashManifestRawAudioSource?, - subtitleSource: ISubtitleSource?, - resumePosition: Double, - speed: Double?, - castId: Int, - onLoadingEstimate: ((Int) -> Unit)? = null, - onLoading: ((Boolean) -> Unit)? = null - ): List { - val ad = activeDevice ?: return listOf(); - - cleanExecutors() - _castServer.removeAllHandlers("castDashRaw") - - val url = getLocalUrl(ad); - val id = UUID.randomUUID(); - - val dashPath = "/dash-${id}" - val videoPath = "/video-${id}" - val audioPath = "/audio-${id}" - val subtitlePath = "/subtitle-${id}" - - val dashUrl = url + dashPath; - Logger.i(TAG, "DASH url: $dashUrl"); - - val videoUrl = url + videoPath - val audioUrl = url + audioPath - - val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) { - return@withContext subtitleSource.getSubtitlesURI(); - } else null; - - var subtitlesUrl: String? = null; - if (subtitlesUri != null) { - if (subtitlesUri.scheme == "file") { - var content: String? = null; - val inputStream = contentResolver.openInputStream(subtitlesUri); - inputStream?.use { stream -> - val reader = stream.bufferedReader(); - content = reader.use { it.readText() }; - } - - if (content != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler( - "GET", - subtitlePath, - content!!, - subtitleSource?.format ?: "text/vtt" - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("cast"); - } - - subtitlesUrl = url + subtitlePath; - } else { - subtitlesUrl = subtitlesUri.toString(); - } - } - - var dashContent: String = withContext(Dispatchers.IO) { - stopVideo() - - //TODO: Include subtitlesURl in the future - val deferred = if (audioSource != null && videoSource != null) { - JSDashManifestMergingRawSource(videoSource, audioSource).generateAsync(_scopeIO) - } else if (audioSource != null) { - audioSource.generateAsync(_scopeIO) - } else if (videoSource != null) { - videoSource.generateAsync(_scopeIO) - } else { - Logger.e(TAG, "Expected at least audio or video to be set") - null - } - - if (deferred != null) { - try { - withContext(Dispatchers.Main) { - if (deferred.estDuration >= 0) { - onLoadingEstimate?.invoke(deferred.estDuration) - } else { - onLoading?.invoke(true) - } - } - deferred.await() - } finally { - if (castId == _castId.get()) { - withContext(Dispatchers.Main) { - onLoading?.invoke(false) - } - } - } - } else { - return@withContext null - } - } ?: throw Exception("Dash is null") - - if (castId != _castId.get()) { - Log.i(TAG, "Get DASH cancelled.") - return emptyList() - } - - for (representation in representationRegex.findAll(dashContent)) { - val mediaType = - representation.groups[1]?.value ?: throw Exception("Media type should be found") - dashContent = mediaInitializationRegex.replace(dashContent) { - if (it.range.first < representation.range.first || it.range.last > representation.range.last) { - return@replace it.value - } - - if (mediaType.startsWith("video/")) { - return@replace "${it.groups[1]!!.value}=\"${videoUrl}?url=${ - URLEncoder.encode( - it.groups[2]!!.value, - "UTF-8" - ).replace("%24Number%24", "\$Number\$") - }&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\"" - } else if (mediaType.startsWith("audio/")) { - return@replace "${it.groups[1]!!.value}=\"${audioUrl}?url=${ - URLEncoder.encode( - it.groups[2]!!.value, - "UTF-8" - ).replace("%24Number%24", "\$Number\$") - }&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\"" - } else { - throw Exception("Expected audio or video") - } - } - } - - if (videoSource != null && !videoSource.hasRequestExecutor) { - throw Exception("Video source without request executor not supported") - } - - if (audioSource != null && !audioSource.hasRequestExecutor) { - throw Exception("Audio source without request executor not supported") - } - - if (audioSource != null && audioSource.hasRequestExecutor) { - _audioExecutor = audioSource.getRequestExecutor() - } - - if (videoSource != null && videoSource.hasRequestExecutor) { - _videoExecutor = videoSource.getRequestExecutor() - } - - //TOOD: Else also handle the non request executor case, perhaps add ?url=$originalUrl to the query parameters, ... propagate this for all other flows also - - Logger.v(TAG) { "Dash manifest: $dashContent" }; - - _castServer.addHandlerWithAllowAllOptions( - HttpConstantHandler( - "GET", dashPath, dashContent, - "application/dash+xml" - ) - .withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castDashRaw"); - - if (videoSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpFunctionHandler("GET", videoPath) { httpContext -> - val originalUrl = - httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } - ?: return@HttpFunctionHandler - val mediaType = - httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } - ?: return@HttpFunctionHandler - - val videoExecutor = _videoExecutor; - if (videoExecutor != null) { - val data = videoExecutor.executeRequest( - "GET", - originalUrl, - null, - httpContext.headers - ) - httpContext.respondBytes(200, HttpHeaders().apply { - put("Content-Type", mediaType) - }, data); - } else { - throw NotImplementedError() - } - }.withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castDashRaw"); - } - if (audioSource != null) { - _castServer.addHandlerWithAllowAllOptions( - HttpFunctionHandler("GET", audioPath) { httpContext -> - val originalUrl = - httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } - ?: return@HttpFunctionHandler - val mediaType = - httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } - ?: return@HttpFunctionHandler - - val audioExecutor = _audioExecutor; - if (audioExecutor != null) { - val data = audioExecutor.executeRequest( - "GET", - originalUrl, - null, - httpContext.headers - ) - httpContext.respondBytes(200, HttpHeaders().apply { - put("Content-Type", mediaType) - }, data); - } else { - throw NotImplementedError() - } - }.withHeader("Access-Control-Allow-Origin", "*"), true - ).withTag("castDashRaw"); - } - - Logger.i( - TAG, - "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath)." - ); - ad.loadVideo( - "application/dash+xml", - dashUrl, - resumePosition, - speed, - metadataFromVideo(video) - ); - - return listOf() - } - - private fun deviceFromCastingDeviceInfo(deviceInfo: com.futo.platformplayer.models.CastingDeviceInfo): CastingDeviceHandle { - val rsAddrs = - deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) } // Throws! - 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 CastingDeviceHandle(_context.createDeviceFromInfo(rsDeviceInfo)) - } - - fun enableDeveloper(enableDev: Boolean) { - _castServer.removeAllHandlers("dev"); - if (enableDev) { - _castServer.addHandler(HttpFunctionHandler("GET", "/dashPlayer") { context -> - if (context.query.containsKey("dashUrl")) { - val dashUrl = context.query["dashUrl"]; - val html = "
\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
"; - context.respondCode(200, html, "text/html"); - } - }).withTag("dev"); - } - } - - companion object { - val instance: ExpStateCasting = ExpStateCasting(); - - private val representationRegex = Regex( - "(.*?)<\\/Representation>", - RegexOption.DOT_MATCHES_ALL - ) - private val mediaInitializationRegex = - Regex("(media|initiali[sz]ation)=\"([^\"]+)\"", RegexOption.DOT_MATCHES_ALL); - - private val TAG = "ExperimentalStateCasting"; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/experimental_casting/StateCastingDispatcher.kt b/app/src/main/java/com/futo/platformplayer/experimental_casting/StateCastingDispatcher.kt deleted file mode 100644 index 1adde666..00000000 --- a/app/src/main/java/com/futo/platformplayer/experimental_casting/StateCastingDispatcher.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.futo.platformplayer.experimental_casting - -import com.futo.platformplayer.Settings -import com.futo.platformplayer.casting.CastConnectionState -import com.futo.platformplayer.casting.StateCasting -import org.fcast.sender_sdk.DeviceFeature - -class StateCastingDispatcher { - companion object { - fun canActiveDeviceSetSpeed(): Boolean { - return if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.activeDevice?.device?.supportsFeature(DeviceFeature.SET_SPEED) == true - } else { - StateCasting.instance.activeDevice?.canSetSpeed == true - } - } - - fun getActiveDeviceSpeed(): Double? { - return if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.activeDevice?.speed - } else { - StateCasting.instance.activeDevice?.speed - } - } - - fun activeDeviceSetSpeed(speed: Double) { - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.activeDevice?.device?.changeSpeed(speed) - } else { - StateCasting.instance.activeDevice?.changeSpeed(speed) - } - } - - fun resumeVideo(): Boolean { - return try { - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.resumeVideo() - } else { - StateCasting.instance.resumeVideo() - } - } catch (_: Throwable) { - false - } - } - - fun pauseVideo(): Boolean { - return try { - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.pauseVideo() - } else { - StateCasting.instance.pauseVideo() - } - } catch (_: Throwable) { - false - } - } - - fun videoSeekTo(timeSeconds: Double): Boolean { - return try { - return if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.videoSeekTo(timeSeconds) - } else { - StateCasting.instance.videoSeekTo(timeSeconds) - } - } catch (_: Throwable) { - false - } - } - - fun stopVideo(): Boolean { - return try { - return if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.stopVideo() - } else { - StateCasting.instance.stopVideo() - } - } catch (_: Throwable) { - false - } - } - - fun isCasting(): Boolean { - return if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.isCasting - } else { - StateCasting.instance.isCasting - } - } - - fun isConnected(): Boolean { - return if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.activeDevice?.connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED - } else { - StateCasting.instance.activeDevice?.connectionState == CastConnectionState.CONNECTED - } - } - - fun isPlaying(): Boolean { - return if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.activeDevice?.isPlaying == true - } else { - StateCasting.instance.activeDevice?.isPlaying == true - } - } - - fun getExpectedCurrentTime(): Double? { - return if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.activeDevice?.expectedCurrentTime - } else { - StateCasting.instance.activeDevice?.expectedCurrentTime - } - } - - fun changeVolume(volume: Double) { - try { - if (Settings.instance.casting.experimentalCasting) { - val activeDevice = - ExpStateCasting.instance.activeDevice ?: return; - if (activeDevice.device.supportsFeature(DeviceFeature.SET_VOLUME)) { - activeDevice.device.changeVolume(volume); - } - } else { - val activeDevice = - StateCasting.instance.activeDevice ?: return; - if (activeDevice.canSetVolume) { - activeDevice.changeVolume(volume); - } - } - } catch (_: Throwable) {} - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt index 9293be3b..5d1f95f1 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt @@ -27,10 +27,10 @@ import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.activities.SettingsActivity import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails +import com.futo.platformplayer.casting.OldStateCasting import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.experimental_casting.ExpStateCasting import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.PlatformVideoWithTime import com.futo.platformplayer.models.UrlVideoWithTime diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 4848e748..84029c7a 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -84,6 +84,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.casting.CastConnectionState +import com.futo.platformplayer.casting.OldStateCasting import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 @@ -98,8 +99,6 @@ import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException import com.futo.platformplayer.exceptions.UnsupportedCastException -import com.futo.platformplayer.experimental_casting.ExpStateCasting -import com.futo.platformplayer.experimental_casting.StateCastingDispatcher import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.fixHtmlWhitespace import com.futo.platformplayer.getNowDiffSeconds @@ -177,7 +176,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import org.fcast.sender_sdk.DeviceFeature import userpackage.Protocol import java.time.OffsetDateTime import java.util.Locale @@ -581,7 +579,7 @@ class VideoDetailView : ConstraintLayout { } else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) { val ad = StateCasting.instance.activeDevice if (ad != null) { - ad.seekVideo(chapter.timeEnd) + ad.seekTo(chapter.timeEnd) } else { _player.seekTo((chapter.timeEnd * 1000).toLong()); } @@ -667,94 +665,49 @@ class VideoDetailView : ConstraintLayout { } if (!isInEditMode) { - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState -> - if (_onPauseCalled) { - return@subscribe; - } - - when (connectionState) { - com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> { - loadCurrentVideo(lastPositionMilliseconds); - updatePillButtonVisibilities(); - setCastEnabled(true); - } - com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> { - loadCurrentVideo(lastPositionMilliseconds, playWhenReady = device.isPlaying); - updatePillButtonVisibilities(); - setCastEnabled(false); - - } - else -> {} - } + StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState -> + if (_onPauseCalled) { + return@subscribe; } - ExpStateCasting.instance.onActiveDevicePlayChanged.subscribe(this) { - val activeDevice = StateCasting.instance.activeDevice; - if (activeDevice != null) { - handlePlayChanged(it); - - val v = video; - if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) { - nextVideo(); - } + when (connectionState) { + CastConnectionState.CONNECTED -> { + loadCurrentVideo(lastPositionMilliseconds); + updatePillButtonVisibilities(); + setCastEnabled(true); } - }; + CastConnectionState.DISCONNECTED -> { + loadCurrentVideo(lastPositionMilliseconds, playWhenReady = device.isPlaying); + updatePillButtonVisibilities(); + setCastEnabled(false); - ExpStateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) { - if (_isCasting) { - setLastPositionMilliseconds((it * 1000.0).toLong(), true); - _cast.setTime(lastPositionMilliseconds); - _timeBar.setPosition(it.toLong()); - _timeBar.setBufferedPosition(0); - _timeBar.setDuration(video?.duration ?: 0); - } - }; - } else { - StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState -> - if (_onPauseCalled) { - return@subscribe; - } - - when (connectionState) { - CastConnectionState.CONNECTED -> { - loadCurrentVideo(lastPositionMilliseconds); - updatePillButtonVisibilities(); - setCastEnabled(true); - } - CastConnectionState.DISCONNECTED -> { - loadCurrentVideo(lastPositionMilliseconds, playWhenReady = device.isPlaying); - updatePillButtonVisibilities(); - setCastEnabled(false); - - } - else -> {} } + else -> {} } - - StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) { - val activeDevice = StateCasting.instance.activeDevice; - if (activeDevice != null) { - handlePlayChanged(it); - - val v = video; - if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) { - nextVideo(); - } - } - }; - - StateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) { - if (_isCasting) { - setLastPositionMilliseconds((it * 1000.0).toLong(), true); - _cast.setTime(lastPositionMilliseconds); - _timeBar.setPosition(it.toLong()); - _timeBar.setBufferedPosition(0); - _timeBar.setDuration(video?.duration ?: 0); - } - }; } + StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) { + val activeDevice = StateCasting.instance.activeDevice; + if (activeDevice != null) { + handlePlayChanged(it); + + val v = video; + if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) { + nextVideo(); + } + } + }; + + StateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) { + if (_isCasting) { + setLastPositionMilliseconds((it * 1000.0).toLong(), true); + _cast.setTime(lastPositionMilliseconds); + _timeBar.setPosition(it.toLong()); + _timeBar.setBufferedPosition(0); + _timeBar.setDuration(video?.duration ?: 0); + } + }; + updatePillButtonVisibilities(); _cast.onTimeJobTimeChanged_s.subscribe { @@ -934,7 +887,7 @@ class VideoDetailView : ConstraintLayout { if (ad != null) { val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong()); if(currentChapter?.type == ChapterType.SKIPPABLE) { - ad.seekVideo(currentChapter.timeEnd); + ad.seekTo(currentChapter.timeEnd); } } else { val currentChapter = _player.getCurrentChapter(_player.position); @@ -1218,7 +1171,7 @@ class VideoDetailView : ConstraintLayout { _onPauseCalled = true; _taskLoadVideo.cancel(); - if (StateCastingDispatcher.isCasting()) { + if (StateCasting.instance.isCasting) { return } @@ -1271,15 +1224,9 @@ class VideoDetailView : ConstraintLayout { _container_content_description.cleanup(); _container_content_support.cleanup(); StatePlayer.instance.autoplayChanged.remove(this) - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.onActiveDevicePlayChanged.remove(this); - ExpStateCasting.instance.onActiveDeviceTimeChanged.remove(this); - ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this); - } else { - StateCasting.instance.onActiveDevicePlayChanged.remove(this); - StateCasting.instance.onActiveDeviceTimeChanged.remove(this); - StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this); - } + StateCasting.instance.onActiveDevicePlayChanged.remove(this); + StateCasting.instance.onActiveDeviceTimeChanged.remove(this); + StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this); StateApp.instance.preventPictureInPicture.remove(this); StatePlayer.instance.onQueueChanged.remove(this); StatePlayer.instance.onVideoChanging.remove(this); @@ -2011,7 +1958,7 @@ class VideoDetailView : ConstraintLayout { return; } - if (!StateCastingDispatcher.isCasting()) { + if (!StateCasting.instance.isCasting) { setCastEnabled(false); val isLimitedVersion = StatePlatform.instance.getContentClientOrNull(video.url)?.let { @@ -2087,19 +2034,11 @@ class VideoDetailView : ConstraintLayout { val startId = plugin?.getUnderlyingPlugin()?.runtimeId try { - val castingSucceeded = if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = { - _cast.setLoading(it) - }, onLoadingEstimate = { - _cast.setLoading(it) - }) - } else { - StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = { - _cast.setLoading(it) - }, onLoadingEstimate = { - _cast.setLoading(it) - }) - } + val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = { + _cast.setLoading(it) + }, onLoadingEstimate = { + _cast.setLoading(it) + }) if (castingSucceeded) { withContext(Dispatchers.Main) { @@ -2295,7 +2234,7 @@ class VideoDetailView : ConstraintLayout { } val currentPlaybackRate = (if (_isCasting) { - StateCastingDispatcher.getActiveDeviceSpeed() + StateCasting.instance.activeDevice?.speed } else _player.getPlaybackRate()) ?: 1.0 _overlay_quality_selector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }?.let { (it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString()) @@ -2414,9 +2353,9 @@ class VideoDetailView : ConstraintLayout { ?.distinct() ?.toList() ?: listOf() else audioSources?.toList() ?: listOf(); - val canSetSpeed = !_isCasting || StateCastingDispatcher.canActiveDeviceSetSpeed(); + val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed() ?: false val currentPlaybackRate = if (_isCasting) { - StateCastingDispatcher.getActiveDeviceSpeed() + StateCasting.instance.activeDevice?.speed } else { _player.getPlaybackRate() } @@ -2434,7 +2373,7 @@ class VideoDetailView : ConstraintLayout { setButtons(playbackLabels, String.format(Locale.US, format, currentPlaybackRate)); onClick.subscribe { v -> val currentPlaybackSpeed = if (_isCasting) { - StateCastingDispatcher.getActiveDeviceSpeed() + StateCasting.instance.activeDevice?.speed } else _player.getPlaybackRate(); var playbackSpeedString = v; val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep(); @@ -2443,9 +2382,11 @@ class VideoDetailView : ConstraintLayout { else if(v == "-") playbackSpeedString = String.format(Locale.US, "%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString(); val newPlaybackSpeed = playbackSpeedString.toDouble(); - if (_isCasting && StateCastingDispatcher.canActiveDeviceSetSpeed()) { + if (_isCasting && StateCasting.instance.activeDevice?.canSetSpeed() ?: false) { qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})"); - StateCastingDispatcher.activeDeviceSetSpeed(newPlaybackSpeed) + try { + StateCasting.instance.activeDevice?.changeSpeed(newPlaybackSpeed) + } catch (_: Throwable) {} setSelected(playbackSpeedString); } else { qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})"); @@ -2561,7 +2502,7 @@ class VideoDetailView : ConstraintLayout { //Handlers private fun handlePlay() { Logger.i(TAG, "handlePlay") - if (!StateCastingDispatcher.resumeVideo()) { + if (!StateCasting.instance.resumeVideo()) { _player.play() } @@ -2577,19 +2518,19 @@ class VideoDetailView : ConstraintLayout { private fun handlePause() { Logger.i(TAG, "handlePause") - if (!StateCastingDispatcher.pauseVideo()) { + if (!StateCasting.instance.pauseVideo()) { _player.pause() } } private fun handleSeek(ms: Long) { Logger.i(TAG, "handleSeek(ms=$ms)") - if (!StateCastingDispatcher.videoSeekTo(ms.toDouble() / 1000.0)) { + if (!StateCasting.instance.videoSeekTo(ms.toDouble() / 1000.0)) { _player.seekTo(ms) } } private fun handleStop() { Logger.i(TAG, "handleStop") - if (!StateCastingDispatcher.stopVideo()) { + if (!StateCasting.instance.stopVideo()) { _player.stop() } } @@ -2597,7 +2538,7 @@ class VideoDetailView : ConstraintLayout { private fun handlePlayChanged(playing: Boolean) { Logger.i(TAG, "handlePlayChanged(playing=$playing)") - if (StateCastingDispatcher.isCasting()) { + if (StateCasting.instance.isCasting) { _cast.setIsPlaying(playing); } else { StatePlayer.instance.updateMediaSession( null); @@ -2639,9 +2580,9 @@ class VideoDetailView : ConstraintLayout { fragment.lifecycleScope.launch(Dispatchers.Main) { try { - if (StateCastingDispatcher.isConnected()) { - val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: 0.0 - val speed = StateCastingDispatcher.getActiveDeviceSpeed() ?: 1.0 + if (StateCasting.instance.activeDevice != null) { + val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0 + val speed = StateCasting.instance.activeDevice?.speed ?: 1.0 castIfAvailable( context.contentResolver, video, @@ -2670,9 +2611,9 @@ class VideoDetailView : ConstraintLayout { fragment.lifecycleScope.launch(Dispatchers.Main) { try { - if (StateCastingDispatcher.isConnected()) { - val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: 0.0 - val speed = StateCastingDispatcher.getActiveDeviceSpeed() ?: 1.0 + if (StateCasting.instance.activeDevice != null) { + val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0 + val speed = StateCasting.instance.activeDevice?.speed ?: 1.0 castIfAvailable( context.contentResolver, video, @@ -2702,9 +2643,9 @@ class VideoDetailView : ConstraintLayout { fragment.lifecycleScope.launch(Dispatchers.Main) { try { - if (StateCastingDispatcher.isConnected()) { - val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: 0.0 - val speed = StateCastingDispatcher.getActiveDeviceSpeed() ?: 1.0 + if (StateCasting.instance.activeDevice != null) { + val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0 + val speed = StateCasting.instance.activeDevice?.speed ?: 1.0 castIfAvailable( context.contentResolver, video, diff --git a/app/src/main/java/com/futo/platformplayer/models/CastingDeviceInfo.kt b/app/src/main/java/com/futo/platformplayer/models/CastingDeviceInfo.kt index 72afedcc..4bb5fa5d 100644 --- a/app/src/main/java/com/futo/platformplayer/models/CastingDeviceInfo.kt +++ b/app/src/main/java/com/futo/platformplayer/models/CastingDeviceInfo.kt @@ -1,34 +1,11 @@ package com.futo.platformplayer.models import com.futo.platformplayer.casting.CastProtocolType -import com.futo.platformplayer.experimental_casting.ExpCastProtocolType @kotlinx.serialization.Serializable -class CastingDeviceInfo { - var name: String; - var type: CastProtocolType; - var addresses: Array; - var port: Int; - - constructor(name: String, type: CastProtocolType, addresses: Array, port: Int) { - this.name = name; - this.type = type; - this.addresses = addresses; - this.port = port; - } -} - -@kotlinx.serialization.Serializable -class ExpCastingDeviceInfo { - var name: String; - var type: ExpCastProtocolType; - var addresses: Array; - var port: Int; - - constructor(name: String, type: ExpCastProtocolType, addresses: Array, port: Int) { - this.name = name; - this.type = type; - this.addresses = addresses; - this.port = port; - } -} +class CastingDeviceInfo( + var name: String, + var type: CastProtocolType, + var addresses: Array, + var port: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index ed9f8922..b1b87d4c 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -33,11 +33,11 @@ import com.futo.platformplayer.activities.SettingsActivity.Companion.settingsAct import com.futo.platformplayer.api.media.platforms.js.DevJSClient import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.background.BackgroundWorker +import com.futo.platformplayer.casting.OldStateCasting import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException -import com.futo.platformplayer.experimental_casting.ExpStateCasting import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment import com.futo.platformplayer.logging.AndroidLogConsumer @@ -760,11 +760,7 @@ class StateApp { _connectivityManager?.unregisterNetworkCallback(_connectivityEvents); StatePlayer.instance.closeMediaSession(); - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.stop() - } else { - StateCasting.instance.stop() - } + StateCasting.instance.stop() StateSync.instance.stop(); StatePlayer.dispose(); Companion.dispose(); diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceAdapter.kt index 06c4974d..a2ff2435 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceAdapter.kt @@ -6,34 +6,14 @@ import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.R import com.futo.platformplayer.casting.CastingDevice import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.experimental_casting.CastingDeviceHandle -sealed class GenericCastingDevice { - class Normal(val device: CastingDevice): GenericCastingDevice() - class Experimental(val handle: CastingDeviceHandle): GenericCastingDevice() - - fun name(): String? { - return when (this) { - is Experimental -> this.handle.device.name() - is Normal -> this.device.name - } - } - - fun isReady(): Boolean { - return when(this) { - is Experimental -> this.handle.device.isReady() - is Normal -> this.device.isReady - } - } -} - -data class DeviceAdapterEntry(val castingDevice: GenericCastingDevice, val isPinnedDevice: Boolean, val isOnlineDevice: Boolean) +data class DeviceAdapterEntry(val castingDevice: CastingDevice, val isPinnedDevice: Boolean, val isOnlineDevice: Boolean) class DeviceAdapter : RecyclerView.Adapter { private val _devices: List; - var onPin = Event1(); - var onConnect = Event1(); + var onPin = Event1(); + var onConnect = Event1(); constructor(devices: List) : super() { _devices = devices; 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 4e0cc26b..4a260fe1 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,15 +9,13 @@ 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.AirPlayCastingDevice import com.futo.platformplayer.casting.CastConnectionState -import com.futo.platformplayer.casting.ChromecastCastingDevice -import com.futo.platformplayer.casting.FCastCastingDevice +import com.futo.platformplayer.casting.CastProtocolType +import com.futo.platformplayer.casting.CastingDevice import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.experimental_casting.ExpStateCasting -import org.fcast.sender_sdk.ProtocolType class DeviceViewHolder : ViewHolder { private val _layoutDevice: FrameLayout; @@ -31,11 +29,11 @@ class DeviceViewHolder : ViewHolder { private var _animatableLoader: Animatable? = null; private var _imagePin: ImageView; - var device: GenericCastingDevice? = null + var device: CastingDevice? = null private set - var onPin = Event1(); - val onConnect = Event1(); + var onPin = Event1(); + val onConnect = Event1(); constructor(view: View) : super(view) { _root = view.findViewById(R.id.layout_root); @@ -55,41 +53,17 @@ class DeviceViewHolder : ViewHolder { val connect = { device?.let { dev -> - when (dev) { - is GenericCastingDevice.Normal -> { - if (dev.device.isReady) { - // NOTE: we assume normal casting is used - StateCasting.instance.activeDevice?.stopCasting() - StateCasting.instance.connectDevice(dev.device) - onConnect.emit(dev) - } else { - try { - view.context?.let { UIDialogs.toast(it, "Device not ready, may be offline") } - } catch (e: Throwable) { - //Ignored - } + try { + if (dev.isReady) { + StateCasting.instance.activeDevice?.stopPlayback() + StateCasting.instance.connectDevice(dev) + onConnect.emit(dev) + } else { + view.context?.let { + UIDialogs.toast(it, "Device not ready, may be offline") } } - is GenericCastingDevice.Experimental -> { - if (dev.handle.device.isReady()) { - // NOTE: we assume experimental casting is used - try { - ExpStateCasting.instance.activeDevice?.device?.stopPlayback() - ExpStateCasting.instance.activeDevice?.device?.disconnect() - } catch (e: Throwable) { - //Ignored - } - ExpStateCasting.instance.connectDevice(dev.handle) - onConnect.emit(dev) - } else { - try { - view.context?.let { UIDialogs.toast(it, "Device not ready, may be offline") } - } catch (e: Throwable) { - //Ignored - } - } - } - } + } catch (_: Throwable) { } } } @@ -103,122 +77,69 @@ class DeviceViewHolder : ViewHolder { } } - // fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) { - - fun bind(d: GenericCastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) { - when (d) { - is GenericCastingDevice.Normal -> { - if (d.device is ChromecastCastingDevice) { - _imageDevice.setImageResource(R.drawable.ic_chromecast); - _textType.text = "Chromecast"; - } else if (d.device is AirPlayCastingDevice) { - _imageDevice.setImageResource(R.drawable.ic_airplay); - _textType.text = "AirPlay"; - } else if (d.device is FCastCastingDevice) { - _imageDevice.setImageResource(R.drawable.ic_fc); - _textType.text = "FCast"; - } - - _textName.text = d.device.name; - _imageOnline.visibility = if (isOnlineDevice && d.device.isReady) View.VISIBLE else View.GONE - - if (!d.device.isReady) { - _imageLoader.visibility = View.GONE; - _textNotReady.visibility = View.VISIBLE; - _imagePin.visibility = View.GONE; - } else { - _textNotReady.visibility = View.GONE; - - val dev = StateCasting.instance.activeDevice; - if (dev == d.device) { - if (dev.connectionState == CastConnectionState.CONNECTED) { - _imageLoader.visibility = View.GONE; - _textNotReady.visibility = View.GONE; - _imagePin.visibility = View.VISIBLE; - } else { - _imageLoader.visibility = View.VISIBLE; - _textNotReady.visibility = View.GONE; - _imagePin.visibility = View.VISIBLE; - } - } else { - if (d.device.isReady) { - _imageLoader.visibility = View.GONE; - _textNotReady.visibility = View.GONE; - _imagePin.visibility = View.VISIBLE; - } else { - _imageLoader.visibility = View.GONE; - _textNotReady.visibility = View.VISIBLE; - _imagePin.visibility = View.VISIBLE; - } - } - - _imagePin.setImageResource(if (isPinnedDevice) R.drawable.keep_24px else R.drawable.ic_pin) - - if (_imageLoader.isVisible) { - _animatableLoader?.start(); - } else { - _animatableLoader?.stop(); - } - } - - device = d; + fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) { + when (d.protocolType) { + CastProtocolType.CHROMECAST -> { + _imageDevice.setImageResource(R.drawable.ic_chromecast); + _textType.text = "Chromecast"; } - is GenericCastingDevice.Experimental -> { - when (d.handle.device.castingProtocol()) { - ProtocolType.CHROMECAST -> { - _imageDevice.setImageResource(R.drawable.ic_chromecast); - _textType.text = "Chromecast"; - } - ProtocolType.F_CAST -> { - _imageDevice.setImageResource(R.drawable.ic_exp_fc); - _textType.text = "FCast"; - } - } - - _textName.text = d.handle.device.name(); - _imageOnline.visibility = if (isOnlineDevice && d.handle.device.isReady()) View.VISIBLE else View.GONE - - if (!d.handle.device.isReady()) { - _imageLoader.visibility = View.GONE; - _textNotReady.visibility = View.VISIBLE; - _imagePin.visibility = View.GONE; + CastProtocolType.AIRPLAY -> { + _imageDevice.setImageResource(R.drawable.ic_airplay); + _textType.text = "AirPlay"; + } + CastProtocolType.FCAST -> { + if (Settings.instance.casting.experimentalCasting) { + _imageDevice.setImageResource(R.drawable.ic_exp_fc) } else { - _textNotReady.visibility = View.GONE; - - val dev = ExpStateCasting.instance.activeDevice; - if (dev == d.handle) { - if (dev.connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED) { - _imageLoader.visibility = View.GONE; - _textNotReady.visibility = View.GONE; - _imagePin.visibility = View.VISIBLE; - } else { - _imageLoader.visibility = View.VISIBLE; - _textNotReady.visibility = View.GONE; - _imagePin.visibility = View.VISIBLE; - } - } else { - if (d.handle.device.isReady()) { - _imageLoader.visibility = View.GONE; - _textNotReady.visibility = View.GONE; - _imagePin.visibility = View.VISIBLE; - } else { - _imageLoader.visibility = View.GONE; - _textNotReady.visibility = View.VISIBLE; - _imagePin.visibility = View.VISIBLE; - } - } - - _imagePin.setImageResource(if (isPinnedDevice) R.drawable.keep_24px else R.drawable.ic_pin) - - if (_imageLoader.isVisible) { - _animatableLoader?.start(); - } else { - _animatableLoader?.stop(); - } + _imageDevice.setImageResource(R.drawable.ic_fc); } - - device = d; + _textType.text = "FCast"; } } + + _textName.text = d.name; + _imageOnline.visibility = if (isOnlineDevice && d.isReady) View.VISIBLE else View.GONE + + + if (!d.isReady) { + _imageLoader.visibility = View.GONE; + _textNotReady.visibility = View.VISIBLE; + _imagePin.visibility = View.GONE; + } else { + _textNotReady.visibility = View.GONE; + + val dev = StateCasting.instance.activeDevice; + if (dev == d) { + if (dev.connectionState == CastConnectionState.CONNECTED) { + _imageLoader.visibility = View.GONE; + _textNotReady.visibility = View.GONE; + _imagePin.visibility = View.VISIBLE; + } else { + _imageLoader.visibility = View.VISIBLE; + _textNotReady.visibility = View.GONE; + _imagePin.visibility = View.VISIBLE; + } + } else { + if (d.isReady) { + _imageLoader.visibility = View.GONE; + _textNotReady.visibility = View.GONE; + _imagePin.visibility = View.VISIBLE; + } else { + _imageLoader.visibility = View.GONE; + _textNotReady.visibility = View.VISIBLE; + _imagePin.visibility = View.VISIBLE; + } + } + + _imagePin.setImageResource(if (isPinnedDevice) R.drawable.keep_24px else R.drawable.ic_pin) + + if (_imageLoader.isVisible) { + _animatableLoader?.start(); + } else { + _animatableLoader?.stop(); + } + } + + device = d; } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/casting/CastButton.kt b/app/src/main/java/com/futo/platformplayer/views/casting/CastButton.kt index 7184eea8..68e249ee 100644 --- a/app/src/main/java/com/futo/platformplayer/views/casting/CastButton.kt +++ b/app/src/main/java/com/futo/platformplayer/views/casting/CastButton.kt @@ -2,21 +2,16 @@ package com.futo.platformplayer.views.casting import android.content.Context import android.util.AttributeSet -import android.view.LayoutInflater import android.view.View -import android.widget.FrameLayout -import android.widget.ImageButton -import android.widget.LinearLayout -import android.widget.TextView import androidx.core.content.ContextCompat import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs -import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.casting.CastConnectionState +import com.futo.platformplayer.casting.CastConnectionState.* +import com.futo.platformplayer.casting.OldStateCasting import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.experimental_casting.ExpStateCasting class CastButton : androidx.appcompat.widget.AppCompatImageButton { var onClick = Event1>(); @@ -29,14 +24,8 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton { visibility = View.GONE; } - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ -> - updateCastState(); - }; - } else { - StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ -> - updateCastState(); - }; + StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ -> + updateCastState() } updateCastState(); @@ -45,47 +34,26 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton { private fun updateCastState() { val c = context ?: return; - if (Settings.instance.casting.experimentalCasting) { - val d = ExpStateCasting.instance.activeDevice; - val activeColor = ContextCompat.getColor(c, R.color.colorPrimary); - val connectingColor = ContextCompat.getColor(c, R.color.gray_c3); - val inactiveColor = ContextCompat.getColor(c, R.color.white); + val d = StateCasting.instance.activeDevice; - if (d != null) { - when (d.connectionState) { - com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> setColorFilter(activeColor) - com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTING -> setColorFilter(connectingColor) - com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> setColorFilter(activeColor) - } - } else { - setColorFilter(inactiveColor); + val activeColor = ContextCompat.getColor(c, R.color.colorPrimary); + val connectingColor = ContextCompat.getColor(c, R.color.gray_c3); + val inactiveColor = ContextCompat.getColor(c, R.color.white); + + if (d != null) { + when (d.connectionState) { + DISCONNECTED -> setColorFilter(activeColor) + CONNECTING -> setColorFilter(connectingColor) + CONNECTED -> setColorFilter(activeColor) } } else { - val d = StateCasting.instance.activeDevice; - - val activeColor = ContextCompat.getColor(c, R.color.colorPrimary); - val connectingColor = ContextCompat.getColor(c, R.color.gray_c3); - val inactiveColor = ContextCompat.getColor(c, R.color.white); - - if (d != null) { - when (d.connectionState) { - CastConnectionState.CONNECTED -> setColorFilter(activeColor) - CastConnectionState.CONNECTING -> setColorFilter(connectingColor) - CastConnectionState.DISCONNECTED -> setColorFilter(activeColor) - } - } else { - setColorFilter(inactiveColor); - } + setColorFilter(inactiveColor); } } fun cleanup() { setOnClickListener(null); - if (Settings.instance.casting.experimentalCasting) { - ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this); - } else { - StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this); - } + StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this); } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt index f66995d7..f09b4689 100644 --- a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt @@ -21,17 +21,12 @@ import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails -import com.futo.platformplayer.casting.AirPlayCastingDevice -import com.futo.platformplayer.casting.ChromecastCastingDevice +import com.futo.platformplayer.casting.CastConnectionState import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event2 -import com.futo.platformplayer.experimental_casting.ExpStateCasting -import com.futo.platformplayer.experimental_casting.StateCastingDispatcher import com.futo.platformplayer.formatDuration -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.views.TargetTapLoaderView import com.futo.platformplayer.views.behavior.GestureControlView @@ -39,9 +34,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.fcast.sender_sdk.DeviceFeature class CastView : ConstraintLayout { private val _thumbnail: ImageView; @@ -100,51 +93,40 @@ class CastView : ConstraintLayout { _gestureControlView.fullScreenGestureEnabled = false _gestureControlView.setupTouchArea(); _gestureControlView.onSpeedHoldStart.subscribe { - if (Settings.instance.casting.experimentalCasting) { - val d = ExpStateCasting.instance.activeDevice ?: return@subscribe; - _speedHoldWasPlaying = d.isPlaying - _speedHoldPrevRate = d.speed - if (d.device.supportsFeature(DeviceFeature.SET_SPEED)) { - try { - d.device.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed()) - } catch (e: Throwable) { - // Ignored - } - } + val d = StateCasting.instance.activeDevice ?: return@subscribe + _speedHoldWasPlaying = d.isPlaying + _speedHoldPrevRate = d.speed + if (d.canSetSpeed()) { try { - d.device.resumePlayback() + d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed()) } catch (e: Throwable) { // Ignored } - } else { - val d = StateCasting.instance.activeDevice ?: return@subscribe; - _speedHoldWasPlaying = d.isPlaying - _speedHoldPrevRate = d.speed - if (d.canSetSpeed) { - d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed()) - } - d.resumeVideo() + } + try { + d.resumePlayback() + } catch (e: Throwable) { + // Ignored } } _gestureControlView.onSpeedHoldEnd.subscribe { - if (Settings.instance.casting.experimentalCasting) { - val d = ExpStateCasting.instance.activeDevice ?: return@subscribe; - if (!_speedHoldWasPlaying) { - d.device.resumePlayback() - } - d.device.changeSpeed(_speedHoldPrevRate) - } else { + try { val d = StateCasting.instance.activeDevice ?: return@subscribe; if (!_speedHoldWasPlaying) { - d.pauseVideo() + d.resumePlayback() } d.changeSpeed(_speedHoldPrevRate) + } catch (e: Throwable) { + // Ignored } } _gestureControlView.onSeek.subscribe { - val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: return@subscribe - StateCastingDispatcher.videoSeekTo(expectedCurrentTime + it / 1000) + try { + val d = StateCasting.instance.activeDevice ?: return@subscribe + val expectedCurrentTime = d.expectedCurrentTime + d.seekTo(expectedCurrentTime + it / 1000) + } catch (_: Throwable) { } }; _buttonLoop.setOnClickListener { @@ -155,25 +137,35 @@ class CastView : ConstraintLayout { _timeBar.addListener(object : TimeBar.OnScrubListener { override fun onScrubStart(timeBar: TimeBar, position: Long) { - StateCastingDispatcher.videoSeekTo(position.toDouble()) + try { + StateCasting.instance.activeDevice?.seekTo(position.toDouble()) + } catch (_: Throwable) { } } override fun onScrubMove(timeBar: TimeBar, position: Long) { - StateCastingDispatcher.videoSeekTo(position.toDouble()) + try { + StateCasting.instance.activeDevice?.seekTo(position.toDouble()) + } catch (_: Throwable) { } } override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { - StateCastingDispatcher.videoSeekTo(position.toDouble()) + try { + StateCasting.instance.activeDevice?.seekTo(position.toDouble()) + } catch (_: Throwable) { } } }); _buttonMinimize.setOnClickListener { onMinimizeClick.emit(); }; _buttonSettings.setOnClickListener { onSettingsClick.emit(); }; _buttonPlay.setOnClickListener { - StateCastingDispatcher.resumeVideo() + try { + StateCasting.instance.activeDevice?.resumePlayback() + } catch (_: Throwable) { } } _buttonPause.setOnClickListener { - StateCastingDispatcher.pauseVideo() + try { + StateCasting.instance.activeDevice?.pausePlayback() + } catch (_: Throwable) { } } if (!isInEditMode) { @@ -257,25 +249,9 @@ class CastView : ConstraintLayout { stopTimeJob() if(isPlaying) { - // NOTE: the experimental implementation polls automatically - if (!Settings.instance.casting.experimentalCasting) { - val d = StateCasting.instance.activeDevice; - if (d is AirPlayCastingDevice || d is ChromecastCastingDevice) { - _updateTimeJob = _scope.launch { - while (true) { - val device = StateCasting.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()) - } - } - } - } + StateCasting.instance.startUpdateTimeJob( + onTimeJobTimeChanged_s + ) { setTime(it) } if (!_inPictureInPicture) { _buttonPause.visibility = View.VISIBLE; @@ -287,7 +263,7 @@ class CastView : ConstraintLayout { _buttonPlay.visibility = View.VISIBLE; } - val position = StateCastingDispatcher.getExpectedCurrentTime()?.times(1000.0)?.toLong() + val position = StateCasting.instance.activeDevice?.expectedCurrentTime?.times(1000.0)?.toLong() if(StatePlayer.instance.hasMediaSession()) { StatePlayer.instance.updateMediaSession(null); StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), (position ?: 0)); @@ -351,10 +327,10 @@ class CastView : ConstraintLayout { } private fun getPlaybackStateCompat(): Int { - if (!StateCastingDispatcher.isConnected()) { + if (StateCasting.instance.activeDevice?.connectionState != CastConnectionState.CONNECTED) { return PlaybackState.STATE_NONE } - return when(StateCastingDispatcher.isPlaying()) { + return when(StateCasting.instance.activeDevice?.isPlaying) { true -> PlaybackStateCompat.STATE_PLAYING; else -> PlaybackStateCompat.STATE_PAUSED; }