casting: refactor SDK integration

This commit is contained in:
Marcus Hanestad 2025-09-03 18:06:17 +02:00
commit e2a5665516
25 changed files with 2508 additions and 3837 deletions

View file

@ -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,8 +437,7 @@ class UIDialogs {
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
if (Settings.instance.casting.experimentalCasting) {
val d = ExpStateCasting.instance.activeDevice;
val d = StateCasting.instance.activeDevice
if (d != null) {
val dialog = ConnectedCastingDialog(context);
if (context is Activity) {
@ -463,32 +461,6 @@ class UIDialogs {
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();
}
}
}
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {

View file

@ -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,12 +506,8 @@ 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)
}
}
StatePlatform.instance.onDevSourceChanged.subscribe {
Logger.i(TAG, "onDevSourceChanged")
@ -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)

View file

@ -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;

View file

@ -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<CastProtocolType> {
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<CastConnectionState>
abstract val onPlayChanged: Event1<Boolean>
abstract val onTimeChanged: Event1<Double>
abstract val onDurationChanged: Event1<Double>
abstract val onVolumeChanged: Event1<Double>
abstract val onSpeedChanged: Event1<Double>
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<InetAddress>
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);
}
};
var onConnectionStateChanged = Event1<CastConnectionState>();
var onPlayChanged = Event1<Boolean>();
var onTimeChanged = Event1<Double>();
var onDurationChanged = Event1<Double>();
var onVolumeChanged = Event1<Double>();
var onSpeedChanged = Event1<Double>();
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<InetAddress>;
abstract fun ensureThreadStarted()
}

View file

@ -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;

View file

@ -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<DeviceConnectionState>();
var onPlayChanged = Event1<Boolean>()
var onTimeChanged = Event1<Double>()
var onDurationChanged = Event1<Double>()
var onVolumeChanged = Event1<Double>()
var onSpeedChanged = Event1<Double>()
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<CastConnectionState>()
override val onPlayChanged: Event1<Boolean>
get() = eventHandler.onPlayChanged
override val onTimeChanged: Event1<Double>
get() = eventHandler.onTimeChanged
override val onDurationChanged: Event1<Double>
get() = eventHandler.onDurationChanged
override val onVolumeChanged: Event1<Double>
get() = eventHandler.onVolumeChanged
override val onSpeedChanged: Event1<Double>
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<InetAddress> = 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"
}
}

View file

@ -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<Long>,
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"
}
}

View file

@ -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;

View file

@ -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<CastProtocolType> {
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<CastConnectionState>();
var onPlayChanged = Event1<Boolean>();
var onTimeChanged = Event1<Double>();
var onDurationChanged = Event1<Double>();
var onVolumeChanged = Event1<Double>();
var onSpeedChanged = Event1<Double>();
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<InetAddress>;
}
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<CastConnectionState> get() = inner.onConnectionStateChanged
override val onPlayChanged: Event1<Boolean> get() = inner.onPlayChanged
override val onTimeChanged: Event1<Double> get() = inner.onTimeChanged
override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged
override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged
override val onSpeedChanged: Event1<Double> 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<InetAddress> = 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 -> {}
}
}

View file

@ -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<FCastNetworkConfig>(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<InetAddress>, 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<Long>,
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<InetAddress>,
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<InetAddress>, 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<InetAddress>, 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<String>,
val services: List<FCastService>
)
@Serializable
private data class FCastService(
val port: Int,
val type: Int
)
companion object {
private val TAG = "OldStateCasting"
}
}

View file

@ -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,15 +110,11 @@ 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)
StateCasting.instance.addRememberedDevice(castingDeviceInfo)
} catch (e: Throwable) {
Logger.e(TAG, "Failed to add remembered device: $e")
}
} else {
StateCasting.instance.addRememberedDevice(castingDeviceInfo)
}
performDismiss();
};

View file

@ -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 isRemembered = _rememberedDevices.contains(d.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
StateCasting.instance.addRememberedDevice(d)
val name = d.name
if (name != null) {
_rememberedDevices.add(name)
}
} else {
StateCasting.instance.removeRememberedDevice(d.device)
_rememberedDevices.remove(d.device.name)
}
}
StateCasting.instance.removeRememberedDevice(d)
_rememberedDevices.remove(d.name)
}
updateUnifiedList()
}
@ -124,60 +103,26 @@ 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 })
_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)
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)
if (name != null) {
_devices.add(name)
updateUnifiedList()
}
}
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name() == d.name }
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name == d.name }
if (index != -1) {
_unifiedDevices[index] = DeviceAdapterEntry(
GenericCastingDevice.Normal(d),
d,
_unifiedDevices[index].isPinnedDevice,
_unifiedDevices[index].isOnlineDevice
)
@ -185,8 +130,8 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
}
}
StateCasting.instance.onDeviceRemoved.subscribe(this) { d ->
_devices.remove(d.name)
StateCasting.instance.onDeviceRemoved.subscribe(this) { deviceName ->
_devices.remove(deviceName.name)
updateUnifiedList()
}
@ -198,23 +143,15 @@ 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)
}
}
private fun updateUnifiedList() {
val oldList = ArrayList(_unifiedDevices)
@ -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,45 +190,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
private fun buildUnifiedList(): List<DeviceAdapterEntry> {
val unifiedList = mutableListOf<DeviceAdapterEntry>()
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 intersectionNames = _devices.intersect(_rememberedDevices)
for (name in intersectionNames) {
onlineDevices[name]?.let {
unifiedList.add(DeviceAdapterEntry(
GenericCastingDevice.Experimental(it), true, true)
)
}
}
val onlineOnlyNames = _devices - _rememberedDevices
for (name in onlineOnlyNames) {
onlineDevices[name]?.let {
unifiedList.add(DeviceAdapterEntry(
GenericCastingDevice.Experimental(it), false, true)
)
}
}
val rememberedOnlyNames = _rememberedDevices - _devices
for (name in rememberedOnlyNames) {
rememberedDevices[name]?.let {
unifiedList.add(DeviceAdapterEntry(
GenericCastingDevice.Experimental(it), true, false)
)
}
}
} else {
val onlineDevices = StateCasting.instance.devices.values.associateBy { it.name }
val rememberedDevices = StateCasting.instance.getRememberedCastingDevices().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)
unifiedList.add(
DeviceAdapterEntry(
it, true, true
)
)
}
}
@ -298,18 +208,22 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
val onlineOnlyNames = _devices - _rememberedDevices
for (name in onlineOnlyNames) {
onlineDevices[name]?.let {
unifiedList.add(DeviceAdapterEntry(
GenericCastingDevice.Normal( it), false, true))
unifiedList.add(
DeviceAdapterEntry(
it, false, true
)
)
}
}
val rememberedOnlyNames = _rememberedDevices - _devices
for (name in rememberedOnlyNames) {
rememberedDevices[name]?.let {
unifiedList.add(DeviceAdapterEntry(
GenericCastingDevice.Normal(it), true, false)
unifiedList.add(
DeviceAdapterEntry(
it, true, false
)
)
}
}
}

View file

@ -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<VideoDetailFragment>()?.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();
}
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 {
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 {
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 {
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)
_device = ad
}
val isConnected = ad != null && ad.connectionState == CastConnectionState.CONNECTED;
setLoading(!isConnected);
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()
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
setLoading(connectionState != CastConnectionState.CONNECTED)
}
updateDevice()
}
updateDevice();
@ -199,81 +171,37 @@ 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;
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;
@ -284,7 +212,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
_sliderPosition.valueTo = dur
if (d.canSetVolume) {
if (d.canSetVolume()) {
_layoutVolumeAdjustable.visibility = View.VISIBLE;
_layoutVolumeFixed.visibility = View.GONE;
} else {
@ -306,13 +234,11 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
CastConnectionState.CONNECTED -> {
enableControls(interactiveControls)
}
CastConnectionState.CONNECTING,
CastConnectionState.DISCONNECTED -> {
CastConnectionState.CONNECTING, CastConnectionState.DISCONNECTED -> {
disableControls(interactiveControls)
}
}
}
}
private fun enableControls(views: List<View>) {
views.forEach { enableControl(it) }

View file

@ -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<DeviceConnectionState>();
var onPlayChanged = Event1<Boolean>()
var onTimeChanged = Event1<Double>()
var onDurationChanged = Event1<Double>()
var onVolumeChanged = Event1<Double>()
var onSpeedChanged = Event1<Double>()
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<ExpCastProtocolType> {
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)
}
}
}
}

View file

@ -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) {}
}
}
}

View file

@ -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

View file

@ -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,50 +665,6 @@ 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 -> {}
}
}
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();
}
}
};
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;
@ -753,7 +707,6 @@ class VideoDetailView : ConstraintLayout {
_timeBar.setDuration(video?.duration ?: 0);
}
};
}
updatePillButtonVisibilities();
@ -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);
}
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 = {
val castingSucceeded = StateCasting.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)
})
}
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,

View file

@ -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<String>;
var port: Int;
constructor(name: String, type: CastProtocolType, addresses: Array<String>, 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<String>;
var port: Int;
constructor(name: String, type: ExpCastProtocolType, addresses: Array<String>, 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<String>,
var port: Int
)

View file

@ -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()
}
StateSync.instance.stop();
StatePlayer.dispose();
Companion.dispose();

View file

@ -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<DeviceViewHolder> {
private val _devices: List<DeviceAdapterEntry>;
var onPin = Event1<GenericCastingDevice>();
var onConnect = Event1<GenericCastingDevice>();
var onPin = Event1<CastingDevice>();
var onConnect = Event1<CastingDevice>();
constructor(devices: List<DeviceAdapterEntry>) : super() {
_devices = devices;

View file

@ -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<GenericCastingDevice>();
val onConnect = Event1<GenericCastingDevice>();
var onPin = Event1<CastingDevice>();
val onConnect = Event1<CastingDevice>();
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)
try {
if (dev.isReady) {
StateCasting.instance.activeDevice?.stopPlayback()
StateCasting.instance.connectDevice(dev)
onConnect.emit(dev)
} else {
try {
view.context?.let { UIDialogs.toast(it, "Device not ready, may be offline") }
} catch (e: Throwable) {
//Ignored
}
}
}
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
}
}
view.context?.let {
UIDialogs.toast(it, "Device not ready, may be offline")
}
}
} catch (_: Throwable) { }
}
}
@ -103,26 +77,31 @@ 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) {
fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
when (d.protocolType) {
CastProtocolType.CHROMECAST -> {
_imageDevice.setImageResource(R.drawable.ic_chromecast);
_textType.text = "Chromecast";
} else if (d.device is AirPlayCastingDevice) {
}
CastProtocolType.AIRPLAY -> {
_imageDevice.setImageResource(R.drawable.ic_airplay);
_textType.text = "AirPlay";
} else if (d.device is FCastCastingDevice) {
}
CastProtocolType.FCAST -> {
if (Settings.instance.casting.experimentalCasting) {
_imageDevice.setImageResource(R.drawable.ic_exp_fc)
} else {
_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
_textName.text = d.name;
_imageOnline.visibility = if (isOnlineDevice && d.isReady) View.VISIBLE else View.GONE
if (!d.device.isReady) {
if (!d.isReady) {
_imageLoader.visibility = View.GONE;
_textNotReady.visibility = View.VISIBLE;
_imagePin.visibility = View.GONE;
@ -130,7 +109,7 @@ class DeviceViewHolder : ViewHolder {
_textNotReady.visibility = View.GONE;
val dev = StateCasting.instance.activeDevice;
if (dev == d.device) {
if (dev == d) {
if (dev.connectionState == CastConnectionState.CONNECTED) {
_imageLoader.visibility = View.GONE;
_textNotReady.visibility = View.GONE;
@ -141,7 +120,7 @@ class DeviceViewHolder : ViewHolder {
_imagePin.visibility = View.VISIBLE;
}
} else {
if (d.device.isReady) {
if (d.isReady) {
_imageLoader.visibility = View.GONE;
_textNotReady.visibility = View.GONE;
_imagePin.visibility = View.VISIBLE;
@ -163,62 +142,4 @@ class DeviceViewHolder : ViewHolder {
device = d;
}
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;
} 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();
}
}
device = d;
}
}
}
}

View file

@ -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<Pair<String, Any>>();
@ -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();
};
updateCastState()
}
updateCastState();
@ -45,23 +34,7 @@ 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);
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);
}
} else {
val d = StateCasting.instance.activeDevice;
val activeColor = ContextCompat.getColor(c, R.color.colorPrimary);
@ -70,22 +43,17 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton {
if (d != null) {
when (d.connectionState) {
CastConnectionState.CONNECTED -> setColorFilter(activeColor)
CastConnectionState.CONNECTING -> setColorFilter(connectingColor)
CastConnectionState.DISCONNECTED -> setColorFilter(activeColor)
DISCONNECTED -> setColorFilter(activeColor)
CONNECTING -> setColorFilter(connectingColor)
CONNECTED -> setColorFilter(activeColor)
}
} else {
setColorFilter(inactiveColor);
}
}
}
fun cleanup() {
setOnClickListener(null);
if (Settings.instance.casting.experimentalCasting) {
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
} else {
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
}
}
}

View file

@ -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;
val d = StateCasting.instance.activeDevice ?: return@subscribe
_speedHoldWasPlaying = d.isPlaying
_speedHoldPrevRate = d.speed
if (d.device.supportsFeature(DeviceFeature.SET_SPEED)) {
if (d.canSetSpeed()) {
try {
d.device.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
} catch (e: Throwable) {
// Ignored
}
}
try {
d.device.resumePlayback()
} 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())
} catch (e: Throwable) {
// Ignored
}
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;
}