mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-09-12 04:22:37 +00:00
casting: refactor SDK integration
This commit is contained in:
parent
1470d5ac74
commit
e2a5665516
25 changed files with 2508 additions and 3837 deletions
|
@ -7,7 +7,6 @@ import android.content.Intent
|
|||
import android.graphics.Color
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.net.Uri
|
||||
import android.text.Layout
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
|
@ -22,6 +21,7 @@ import androidx.core.content.ContextCompat
|
|||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.casting.OldStateCasting
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
||||
|
@ -38,7 +38,6 @@ import com.futo.platformplayer.dialogs.MigrateDialog
|
|||
import com.futo.platformplayer.dialogs.PluginUpdateDialog
|
||||
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||
|
@ -438,56 +437,29 @@ class UIDialogs {
|
|||
|
||||
|
||||
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
val d = ExpStateCasting.instance.activeDevice;
|
||||
if (d != null) {
|
||||
val dialog = ConnectedCastingDialog(context);
|
||||
if (context is Activity) {
|
||||
dialog.setOwnerActivity(context)
|
||||
}
|
||||
registerDialogOpened(dialog);
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
} else {
|
||||
val dialog = ConnectCastingDialog(context);
|
||||
if (context is Activity) {
|
||||
dialog.setOwnerActivity(context)
|
||||
}
|
||||
registerDialogOpened(dialog);
|
||||
val c = context
|
||||
if (c is Activity) {
|
||||
dialog.setOwnerActivity(c);
|
||||
}
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
val d = StateCasting.instance.activeDevice
|
||||
if (d != null) {
|
||||
val dialog = ConnectedCastingDialog(context);
|
||||
if (context is Activity) {
|
||||
dialog.setOwnerActivity(context)
|
||||
}
|
||||
registerDialogOpened(dialog);
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
} else {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null) {
|
||||
val dialog = ConnectedCastingDialog(context);
|
||||
if (context is Activity) {
|
||||
dialog.setOwnerActivity(context)
|
||||
}
|
||||
registerDialogOpened(dialog);
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
} else {
|
||||
val dialog = ConnectCastingDialog(context);
|
||||
if (context is Activity) {
|
||||
dialog.setOwnerActivity(context)
|
||||
}
|
||||
registerDialogOpened(dialog);
|
||||
val c = context
|
||||
if (c is Activity) {
|
||||
dialog.setOwnerActivity(c);
|
||||
}
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
val dialog = ConnectCastingDialog(context);
|
||||
if (context is Activity) {
|
||||
dialog.setOwnerActivity(context)
|
||||
}
|
||||
registerDialogOpened(dialog);
|
||||
val c = context
|
||||
if (c is Activity) {
|
||||
dialog.setOwnerActivity(c);
|
||||
}
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,6 @@ import com.futo.platformplayer.api.http.ManagedHttpClient
|
|||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
||||
|
@ -118,7 +117,6 @@ import java.util.LinkedList
|
|||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
|
||||
//TODO: Move to dimensions
|
||||
|
@ -508,11 +506,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
handleIntent(intent);
|
||||
|
||||
if (Settings.instance.casting.enabled) {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.start(this)
|
||||
} else {
|
||||
StateCasting.instance.start(this)
|
||||
}
|
||||
StateCasting.instance.start(this)
|
||||
}
|
||||
|
||||
StatePlatform.instance.onDevSourceChanged.subscribe {
|
||||
|
@ -1051,11 +1045,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
Logger.i(TAG, "handleFCast");
|
||||
|
||||
try {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.handleUrl(this, url)
|
||||
} else {
|
||||
StateCasting.instance.handleUrl(this, url)
|
||||
}
|
||||
StateCasting.instance.handleUrl(url)
|
||||
return true;
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
abstract fun ensureThreadStarted()
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 -> {}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -11,8 +11,8 @@ import com.futo.platformplayer.R
|
|||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.casting.CastProtocolType
|
||||
import com.futo.platformplayer.casting.OldStateCasting
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
import com.futo.platformplayer.toInetAddress
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
|
@ -110,14 +110,10 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||
|
||||
_textError.visibility = View.GONE;
|
||||
val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt());
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
try {
|
||||
ExpStateCasting.instance.addRememberedDevice(castingDeviceInfo)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to add remembered device: $e")
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
StateCasting.instance.addRememberedDevice(castingDeviceInfo)
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to add remembered device: $e")
|
||||
}
|
||||
performDismiss();
|
||||
};
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
|
@ -15,18 +14,14 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.CastingDevice
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.adapters.DeviceAdapter
|
||||
import com.futo.platformplayer.views.adapters.DeviceAdapterEntry
|
||||
import com.futo.platformplayer.views.adapters.GenericCastingDevice
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -58,33 +53,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
_recyclerDevices.layoutManager = LinearLayoutManager(context);
|
||||
|
||||
_adapter.onPin.subscribe { d ->
|
||||
when (d) {
|
||||
is GenericCastingDevice.Experimental -> {
|
||||
val isRemembered = _rememberedDevices.contains(d.handle.device.name())
|
||||
val newIsRemembered = !isRemembered
|
||||
if (newIsRemembered) {
|
||||
ExpStateCasting.instance.addRememberedDevice(d.handle)
|
||||
val name = d.handle.device.name()
|
||||
_rememberedDevices.add(name)
|
||||
} else {
|
||||
ExpStateCasting.instance.removeRememberedDevice(d.handle)
|
||||
_rememberedDevices.remove(d.handle.device.name())
|
||||
}
|
||||
}
|
||||
is GenericCastingDevice.Normal -> {
|
||||
val isRemembered = _rememberedDevices.contains(d.device.name)
|
||||
val newIsRemembered = !isRemembered
|
||||
if (newIsRemembered) {
|
||||
StateCasting.instance.addRememberedDevice(d.device)
|
||||
val name = d.device.name
|
||||
if (name != null) {
|
||||
_rememberedDevices.add(name)
|
||||
}
|
||||
} else {
|
||||
StateCasting.instance.removeRememberedDevice(d.device)
|
||||
_rememberedDevices.remove(d.device.name)
|
||||
}
|
||||
val isRemembered = _rememberedDevices.contains(d.name)
|
||||
val newIsRemembered = !isRemembered
|
||||
if (newIsRemembered) {
|
||||
StateCasting.instance.addRememberedDevice(d)
|
||||
val name = d.name
|
||||
if (name != null) {
|
||||
_rememberedDevices.add(name)
|
||||
}
|
||||
} else {
|
||||
StateCasting.instance.removeRememberedDevice(d)
|
||||
_rememberedDevices.remove(d.name)
|
||||
}
|
||||
updateUnifiedList()
|
||||
}
|
||||
|
@ -124,77 +103,42 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
|
||||
(_imageLoader.drawable as Animatable?)?.start();
|
||||
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
synchronized(ExpStateCasting.instance.devices) {
|
||||
_devices.addAll(ExpStateCasting.instance.devices.values.map { it.device.name() })
|
||||
}
|
||||
_rememberedDevices.addAll(ExpStateCasting.instance.getRememberedCastingDeviceNames())
|
||||
} else {
|
||||
synchronized(StateCasting.instance.devices) {
|
||||
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
|
||||
}
|
||||
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
||||
synchronized(StateCasting.instance.devices) {
|
||||
_devices.addAll(StateCasting.instance.devices.values.map { it.name.orEmpty() })
|
||||
}
|
||||
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
||||
|
||||
updateUnifiedList()
|
||||
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
||||
_devices.add(d.name())
|
||||
updateUnifiedList()
|
||||
}
|
||||
|
||||
ExpStateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
||||
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name() == d.device.name() }
|
||||
if (index != -1) {
|
||||
val dev = GenericCastingDevice.Experimental(d)
|
||||
_unifiedDevices[index] = DeviceAdapterEntry(dev, _unifiedDevices[index].isPinnedDevice, _unifiedDevices[index].isOnlineDevice)
|
||||
_adapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
ExpStateCasting.instance.onDeviceRemoved.subscribe(this) { deviceName ->
|
||||
_devices.remove(deviceName)
|
||||
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
||||
val name = d.name
|
||||
if (name != null) {
|
||||
_devices.add(name)
|
||||
updateUnifiedList()
|
||||
}
|
||||
}
|
||||
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||
if (connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
||||
val name = d.name
|
||||
if (name != null)
|
||||
_devices.add(name)
|
||||
updateUnifiedList()
|
||||
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
||||
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name == d.name }
|
||||
if (index != -1) {
|
||||
_unifiedDevices[index] = DeviceAdapterEntry(
|
||||
d,
|
||||
_unifiedDevices[index].isPinnedDevice,
|
||||
_unifiedDevices[index].isOnlineDevice
|
||||
)
|
||||
_adapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
||||
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name() == d.name }
|
||||
if (index != -1) {
|
||||
_unifiedDevices[index] = DeviceAdapterEntry(
|
||||
GenericCastingDevice.Normal(d),
|
||||
_unifiedDevices[index].isPinnedDevice,
|
||||
_unifiedDevices[index].isOnlineDevice
|
||||
)
|
||||
_adapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
StateCasting.instance.onDeviceRemoved.subscribe(this) { deviceName ->
|
||||
_devices.remove(deviceName.name)
|
||||
updateUnifiedList()
|
||||
}
|
||||
|
||||
StateCasting.instance.onDeviceRemoved.subscribe(this) { d ->
|
||||
_devices.remove(d.name)
|
||||
updateUnifiedList()
|
||||
}
|
||||
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||
if (connectionState == CastConnectionState.CONNECTED) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
dismiss()
|
||||
}
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||
if (connectionState == CastConnectionState.CONNECTED) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -203,17 +147,10 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
override fun dismiss() {
|
||||
super.dismiss()
|
||||
(_imageLoader.drawable as Animatable?)?.stop()
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onDeviceAdded.remove(this)
|
||||
ExpStateCasting.instance.onDeviceChanged.remove(this)
|
||||
ExpStateCasting.instance.onDeviceRemoved.remove(this)
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this)
|
||||
} else {
|
||||
StateCasting.instance.onDeviceAdded.remove(this)
|
||||
StateCasting.instance.onDeviceChanged.remove(this)
|
||||
StateCasting.instance.onDeviceRemoved.remove(this)
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this)
|
||||
}
|
||||
StateCasting.instance.onDeviceAdded.remove(this)
|
||||
StateCasting.instance.onDeviceChanged.remove(this)
|
||||
StateCasting.instance.onDeviceRemoved.remove(this)
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this)
|
||||
}
|
||||
|
||||
private fun updateUnifiedList() {
|
||||
|
@ -226,16 +163,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList[oldItemPosition]
|
||||
val newItem = newList[newItemPosition]
|
||||
return oldItem.castingDevice.name() == newItem.castingDevice.name()
|
||||
&& oldItem.castingDevice.isReady() == newItem.castingDevice.isReady()
|
||||
return oldItem.castingDevice.name == newItem.castingDevice.name
|
||||
&& oldItem.castingDevice.isReady == newItem.castingDevice.isReady
|
||||
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
|
||||
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList[oldItemPosition]
|
||||
val newItem = newList[newItemPosition]
|
||||
return oldItem.castingDevice.name() == newItem.castingDevice.name()
|
||||
&& oldItem.castingDevice.isReady() == newItem.castingDevice.isReady()
|
||||
return oldItem.castingDevice.name == newItem.castingDevice.name
|
||||
&& oldItem.castingDevice.isReady == newItem.castingDevice.isReady
|
||||
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
|
||||
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
|
||||
}
|
||||
|
@ -252,64 +190,40 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
private fun buildUnifiedList(): List<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 onlineDevices = StateCasting.instance.devices.values.associateBy { it.name }
|
||||
val rememberedDevices =
|
||||
StateCasting.instance.getRememberedCastingDevices().associateBy { it.name }
|
||||
|
||||
val intersectionNames = _devices.intersect(_rememberedDevices)
|
||||
for (name in intersectionNames) {
|
||||
onlineDevices[name]?.let {
|
||||
unifiedList.add(DeviceAdapterEntry(
|
||||
GenericCastingDevice.Experimental(it), true, true)
|
||||
val intersectionNames = _devices.intersect(_rememberedDevices)
|
||||
for (name in intersectionNames) {
|
||||
onlineDevices[name]?.let {
|
||||
unifiedList.add(
|
||||
DeviceAdapterEntry(
|
||||
it, true, true
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val onlineOnlyNames = _devices - _rememberedDevices
|
||||
for (name in onlineOnlyNames) {
|
||||
onlineDevices[name]?.let {
|
||||
unifiedList.add(DeviceAdapterEntry(
|
||||
GenericCastingDevice.Experimental(it), false, true)
|
||||
val onlineOnlyNames = _devices - _rememberedDevices
|
||||
for (name in onlineOnlyNames) {
|
||||
onlineDevices[name]?.let {
|
||||
unifiedList.add(
|
||||
DeviceAdapterEntry(
|
||||
it, false, true
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val rememberedOnlyNames = _rememberedDevices - _devices
|
||||
for (name in rememberedOnlyNames) {
|
||||
rememberedDevices[name]?.let {
|
||||
unifiedList.add(DeviceAdapterEntry(
|
||||
GenericCastingDevice.Experimental(it), true, false)
|
||||
val rememberedOnlyNames = _rememberedDevices - _devices
|
||||
for (name in rememberedOnlyNames) {
|
||||
rememberedDevices[name]?.let {
|
||||
unifiedList.add(
|
||||
DeviceAdapterEntry(
|
||||
it, true, false
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val onlineDevices = StateCasting.instance.devices.values.associateBy { it.name }
|
||||
val rememberedDevices = StateCasting.instance.getRememberedCastingDevices().associateBy { it.name }
|
||||
|
||||
val intersectionNames = _devices.intersect(_rememberedDevices)
|
||||
for (name in intersectionNames) {
|
||||
onlineDevices[name]?.let {
|
||||
unifiedList.add(DeviceAdapterEntry(
|
||||
GenericCastingDevice.Normal(it), true, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val onlineOnlyNames = _devices - _rememberedDevices
|
||||
for (name in onlineOnlyNames) {
|
||||
onlineDevices[name]?.let {
|
||||
unifiedList.add(DeviceAdapterEntry(
|
||||
GenericCastingDevice.Normal( it), false, true))
|
||||
}
|
||||
}
|
||||
|
||||
val rememberedOnlyNames = _rememberedDevices - _devices
|
||||
for (name in rememberedOnlyNames) {
|
||||
rememberedDevices[name]?.let {
|
||||
unifiedList.add(DeviceAdapterEntry(
|
||||
GenericCastingDevice.Normal(it), true, false)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
try {
|
||||
StateCasting.instance.activeDevice?.disconnect()
|
||||
} catch (_: Throwable) {}
|
||||
dismiss();
|
||||
};
|
||||
|
||||
|
@ -114,7 +112,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
return@OnChangeListener
|
||||
}
|
||||
|
||||
StateCastingDispatcher.videoSeekTo(value.toDouble())
|
||||
try {
|
||||
StateCasting.instance.activeDevice?.seekTo(value.toDouble())
|
||||
} catch (_: Throwable) {}
|
||||
});
|
||||
|
||||
//TODO: Check if volume slider is properly hidden in all cases
|
||||
|
@ -123,7 +123,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
return@OnChangeListener
|
||||
}
|
||||
|
||||
StateCastingDispatcher.changeVolume(value.toDouble())
|
||||
try {
|
||||
StateCasting.instance.activeDevice?.changeVolume(value.toDouble())
|
||||
} catch (_: Throwable) {}
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
@ -134,64 +136,34 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
super.show();
|
||||
Logger.i(TAG, "Dialog shown.");
|
||||
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onActiveDeviceVolumeChanged.remove(this)
|
||||
ExpStateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
||||
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo)
|
||||
}
|
||||
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this)
|
||||
StateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
||||
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo)
|
||||
}
|
||||
|
||||
ExpStateCasting.instance.onActiveDeviceTimeChanged.remove(this)
|
||||
ExpStateCasting.instance.onActiveDeviceTimeChanged.subscribe {
|
||||
_sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo)
|
||||
}
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this)
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.subscribe {
|
||||
_sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo)
|
||||
}
|
||||
|
||||
ExpStateCasting.instance.onActiveDeviceDurationChanged.remove(this)
|
||||
ExpStateCasting.instance.onActiveDeviceDurationChanged.subscribe {
|
||||
val dur = it.toFloat().coerceAtLeast(1.0f)
|
||||
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||
_sliderPosition.valueTo = dur
|
||||
}
|
||||
StateCasting.instance.onActiveDeviceDurationChanged.remove(this)
|
||||
StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
|
||||
val dur = it.toFloat().coerceAtLeast(1.0f)
|
||||
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||
_sliderPosition.valueTo = dur
|
||||
}
|
||||
|
||||
val ad = ExpStateCasting.instance.activeDevice
|
||||
if (ad != null) {
|
||||
_device = GenericCastingDevice.Experimental(ad)
|
||||
}
|
||||
val isConnected = ad != null && ad.connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED
|
||||
setLoading(!isConnected)
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
setLoading(connectionState != com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED)
|
||||
}
|
||||
updateDevice()
|
||||
}
|
||||
} else {
|
||||
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
||||
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||
};
|
||||
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.subscribe {
|
||||
_sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo);
|
||||
};
|
||||
|
||||
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
|
||||
val dur = it.toFloat().coerceAtLeast(1.0f)
|
||||
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur);
|
||||
_sliderPosition.valueTo = dur
|
||||
};
|
||||
|
||||
val ad = StateCasting.instance.activeDevice
|
||||
if (ad != null) {
|
||||
_device = GenericCastingDevice.Normal(ad)
|
||||
}
|
||||
val isConnected = ad != null && ad.connectionState == CastConnectionState.CONNECTED;
|
||||
setLoading(!isConnected);
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { setLoading(connectionState != CastConnectionState.CONNECTED); };
|
||||
updateDevice()
|
||||
val ad = StateCasting.instance.activeDevice
|
||||
if (ad != null) {
|
||||
_device = ad
|
||||
}
|
||||
val isConnected = ad != null && ad.connectionState == CastConnectionState.CONNECTED
|
||||
setLoading(!isConnected)
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
setLoading(connectionState != CastConnectionState.CONNECTED)
|
||||
}
|
||||
updateDevice()
|
||||
}
|
||||
|
||||
updateDevice();
|
||||
|
@ -199,117 +171,71 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
|
||||
override fun dismiss() {
|
||||
super.dismiss();
|
||||
|
||||
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
ExpStateCasting.instance.onActiveDeviceVolumeChanged.remove(this);
|
||||
ExpStateCasting.instance.onActiveDeviceDurationChanged.remove(this);
|
||||
ExpStateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
_device = null;
|
||||
}
|
||||
|
||||
private fun updateDevice() {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
val d = ExpStateCasting.instance.activeDevice ?: return;
|
||||
val d = StateCasting.instance.activeDevice ?: return;
|
||||
|
||||
when (d.device.castingProtocol()) {
|
||||
ProtocolType.CHROMECAST -> {
|
||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||
_textType.text = "Chromecast";
|
||||
}
|
||||
ProtocolType.F_CAST -> {
|
||||
_imageDevice.setImageResource(R.drawable.ic_exp_fc);
|
||||
_textType.text = "FCast";
|
||||
}
|
||||
}
|
||||
|
||||
_textName.text = d.device.name();
|
||||
_sliderPosition.valueFrom = 0.0f;
|
||||
_sliderVolume.valueFrom = 0.0f;
|
||||
_sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||
|
||||
val dur = d.duration.toFloat().coerceAtLeast(1.0f)
|
||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||
_sliderPosition.valueTo = dur
|
||||
|
||||
if (d.device.supportsFeature(DeviceFeature.SET_VOLUME)) {
|
||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||
_layoutVolumeFixed.visibility = View.GONE;
|
||||
} else {
|
||||
_layoutVolumeAdjustable.visibility = View.GONE;
|
||||
_layoutVolumeFixed.visibility = View.VISIBLE;
|
||||
}
|
||||
|
||||
val interactiveControls = listOf(
|
||||
_sliderPosition,
|
||||
_sliderVolume,
|
||||
_buttonPrevious,
|
||||
_buttonPlay,
|
||||
_buttonPause,
|
||||
_buttonStop,
|
||||
_buttonNext
|
||||
)
|
||||
|
||||
when (d.connectionState) {
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> {
|
||||
enableControls(interactiveControls)
|
||||
}
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTING,
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> {
|
||||
disableControls(interactiveControls)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val d = StateCasting.instance.activeDevice ?: return;
|
||||
|
||||
if (d is ChromecastCastingDevice) {
|
||||
when (d.protocolType) {
|
||||
CastProtocolType.CHROMECAST -> {
|
||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||
_textType.text = "Chromecast";
|
||||
} else if (d is AirPlayCastingDevice) {
|
||||
}
|
||||
CastProtocolType.AIRPLAY -> {
|
||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||
_textType.text = "AirPlay";
|
||||
} else if (d is FCastCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
||||
}
|
||||
CastProtocolType.FCAST -> {
|
||||
_imageDevice.setImageResource(
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
R.drawable.ic_exp_fc
|
||||
} else {
|
||||
R.drawable.ic_fc
|
||||
}
|
||||
)
|
||||
_textType.text = "FCast";
|
||||
}
|
||||
}
|
||||
|
||||
_textName.text = d.name;
|
||||
_sliderPosition.valueFrom = 0.0f;
|
||||
_sliderVolume.valueFrom = 0.0f;
|
||||
_sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||
_textName.text = d.name;
|
||||
_sliderPosition.valueFrom = 0.0f;
|
||||
_sliderVolume.valueFrom = 0.0f;
|
||||
_sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||
|
||||
val dur = d.duration.toFloat().coerceAtLeast(1.0f)
|
||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||
_sliderPosition.valueTo = dur
|
||||
val dur = d.duration.toFloat().coerceAtLeast(1.0f)
|
||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||
_sliderPosition.valueTo = dur
|
||||
|
||||
if (d.canSetVolume) {
|
||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||
_layoutVolumeFixed.visibility = View.GONE;
|
||||
} else {
|
||||
_layoutVolumeAdjustable.visibility = View.GONE;
|
||||
_layoutVolumeFixed.visibility = View.VISIBLE;
|
||||
if (d.canSetVolume()) {
|
||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||
_layoutVolumeFixed.visibility = View.GONE;
|
||||
} else {
|
||||
_layoutVolumeAdjustable.visibility = View.GONE;
|
||||
_layoutVolumeFixed.visibility = View.VISIBLE;
|
||||
}
|
||||
|
||||
val interactiveControls = listOf(
|
||||
_sliderPosition,
|
||||
_sliderVolume,
|
||||
_buttonPrevious,
|
||||
_buttonPlay,
|
||||
_buttonPause,
|
||||
_buttonStop,
|
||||
_buttonNext
|
||||
)
|
||||
|
||||
when (d.connectionState) {
|
||||
CastConnectionState.CONNECTED -> {
|
||||
enableControls(interactiveControls)
|
||||
}
|
||||
|
||||
val interactiveControls = listOf(
|
||||
_sliderPosition,
|
||||
_sliderVolume,
|
||||
_buttonPrevious,
|
||||
_buttonPlay,
|
||||
_buttonPause,
|
||||
_buttonStop,
|
||||
_buttonNext
|
||||
)
|
||||
|
||||
when (d.connectionState) {
|
||||
CastConnectionState.CONNECTED -> {
|
||||
enableControls(interactiveControls)
|
||||
}
|
||||
CastConnectionState.CONNECTING,
|
||||
CastConnectionState.DISCONNECTED -> {
|
||||
disableControls(interactiveControls)
|
||||
}
|
||||
CastConnectionState.CONNECTING, CastConnectionState.DISCONNECTED -> {
|
||||
disableControls(interactiveControls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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) {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -84,6 +84,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
|||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.OldStateCasting
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
|
@ -98,8 +99,6 @@ import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
|
|||
import com.futo.platformplayer.engine.exceptions.ScriptReloadRequiredException
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.experimental_casting.StateCastingDispatcher
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.fixHtmlWhitespace
|
||||
import com.futo.platformplayer.getNowDiffSeconds
|
||||
|
@ -177,7 +176,6 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fcast.sender_sdk.DeviceFeature
|
||||
import userpackage.Protocol
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Locale
|
||||
|
@ -581,7 +579,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
|
||||
val ad = StateCasting.instance.activeDevice
|
||||
if (ad != null) {
|
||||
ad.seekVideo(chapter.timeEnd)
|
||||
ad.seekTo(chapter.timeEnd)
|
||||
} else {
|
||||
_player.seekTo((chapter.timeEnd * 1000).toLong());
|
||||
}
|
||||
|
@ -667,94 +665,49 @@ class VideoDetailView : ConstraintLayout {
|
|||
}
|
||||
|
||||
if (!isInEditMode) {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState ->
|
||||
if (_onPauseCalled) {
|
||||
return@subscribe;
|
||||
}
|
||||
|
||||
when (connectionState) {
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> {
|
||||
loadCurrentVideo(lastPositionMilliseconds);
|
||||
updatePillButtonVisibilities();
|
||||
setCastEnabled(true);
|
||||
}
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> {
|
||||
loadCurrentVideo(lastPositionMilliseconds, playWhenReady = device.isPlaying);
|
||||
updatePillButtonVisibilities();
|
||||
setCastEnabled(false);
|
||||
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState ->
|
||||
if (_onPauseCalled) {
|
||||
return@subscribe;
|
||||
}
|
||||
|
||||
ExpStateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
||||
val activeDevice = StateCasting.instance.activeDevice;
|
||||
if (activeDevice != null) {
|
||||
handlePlayChanged(it);
|
||||
|
||||
val v = video;
|
||||
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
|
||||
nextVideo();
|
||||
}
|
||||
when (connectionState) {
|
||||
CastConnectionState.CONNECTED -> {
|
||||
loadCurrentVideo(lastPositionMilliseconds);
|
||||
updatePillButtonVisibilities();
|
||||
setCastEnabled(true);
|
||||
}
|
||||
};
|
||||
CastConnectionState.DISCONNECTED -> {
|
||||
loadCurrentVideo(lastPositionMilliseconds, playWhenReady = device.isPlaying);
|
||||
updatePillButtonVisibilities();
|
||||
setCastEnabled(false);
|
||||
|
||||
ExpStateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) {
|
||||
if (_isCasting) {
|
||||
setLastPositionMilliseconds((it * 1000.0).toLong(), true);
|
||||
_cast.setTime(lastPositionMilliseconds);
|
||||
_timeBar.setPosition(it.toLong());
|
||||
_timeBar.setBufferedPosition(0);
|
||||
_timeBar.setDuration(video?.duration ?: 0);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState ->
|
||||
if (_onPauseCalled) {
|
||||
return@subscribe;
|
||||
}
|
||||
|
||||
when (connectionState) {
|
||||
CastConnectionState.CONNECTED -> {
|
||||
loadCurrentVideo(lastPositionMilliseconds);
|
||||
updatePillButtonVisibilities();
|
||||
setCastEnabled(true);
|
||||
}
|
||||
CastConnectionState.DISCONNECTED -> {
|
||||
loadCurrentVideo(lastPositionMilliseconds, playWhenReady = device.isPlaying);
|
||||
updatePillButtonVisibilities();
|
||||
setCastEnabled(false);
|
||||
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
||||
val activeDevice = StateCasting.instance.activeDevice;
|
||||
if (activeDevice != null) {
|
||||
handlePlayChanged(it);
|
||||
|
||||
val v = video;
|
||||
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
|
||||
nextVideo();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) {
|
||||
if (_isCasting) {
|
||||
setLastPositionMilliseconds((it * 1000.0).toLong(), true);
|
||||
_cast.setTime(lastPositionMilliseconds);
|
||||
_timeBar.setPosition(it.toLong());
|
||||
_timeBar.setBufferedPosition(0);
|
||||
_timeBar.setDuration(video?.duration ?: 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
||||
val activeDevice = StateCasting.instance.activeDevice;
|
||||
if (activeDevice != null) {
|
||||
handlePlayChanged(it);
|
||||
|
||||
val v = video;
|
||||
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
|
||||
nextVideo();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) {
|
||||
if (_isCasting) {
|
||||
setLastPositionMilliseconds((it * 1000.0).toLong(), true);
|
||||
_cast.setTime(lastPositionMilliseconds);
|
||||
_timeBar.setPosition(it.toLong());
|
||||
_timeBar.setBufferedPosition(0);
|
||||
_timeBar.setDuration(video?.duration ?: 0);
|
||||
}
|
||||
};
|
||||
|
||||
updatePillButtonVisibilities();
|
||||
|
||||
_cast.onTimeJobTimeChanged_s.subscribe {
|
||||
|
@ -934,7 +887,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
if (ad != null) {
|
||||
val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong());
|
||||
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
||||
ad.seekVideo(currentChapter.timeEnd);
|
||||
ad.seekTo(currentChapter.timeEnd);
|
||||
}
|
||||
} else {
|
||||
val currentChapter = _player.getCurrentChapter(_player.position);
|
||||
|
@ -1218,7 +1171,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
_onPauseCalled = true;
|
||||
_taskLoadVideo.cancel();
|
||||
|
||||
if (StateCastingDispatcher.isCasting()) {
|
||||
if (StateCasting.instance.isCasting) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1271,15 +1224,9 @@ class VideoDetailView : ConstraintLayout {
|
|||
_container_content_description.cleanup();
|
||||
_container_content_support.cleanup();
|
||||
StatePlayer.instance.autoplayChanged.remove(this)
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
||||
ExpStateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
} else {
|
||||
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
}
|
||||
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
StateApp.instance.preventPictureInPicture.remove(this);
|
||||
StatePlayer.instance.onQueueChanged.remove(this);
|
||||
StatePlayer.instance.onVideoChanging.remove(this);
|
||||
|
@ -2011,7 +1958,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!StateCastingDispatcher.isCasting()) {
|
||||
if (!StateCasting.instance.isCasting) {
|
||||
setCastEnabled(false);
|
||||
|
||||
val isLimitedVersion = StatePlatform.instance.getContentClientOrNull(video.url)?.let {
|
||||
|
@ -2087,19 +2034,11 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
|
||||
try {
|
||||
val castingSucceeded = if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
|
||||
_cast.setLoading(it)
|
||||
}, onLoadingEstimate = {
|
||||
_cast.setLoading(it)
|
||||
})
|
||||
} else {
|
||||
StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
|
||||
_cast.setLoading(it)
|
||||
}, onLoadingEstimate = {
|
||||
_cast.setLoading(it)
|
||||
})
|
||||
}
|
||||
val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
|
||||
_cast.setLoading(it)
|
||||
}, onLoadingEstimate = {
|
||||
_cast.setLoading(it)
|
||||
})
|
||||
|
||||
if (castingSucceeded) {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -2295,7 +2234,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
}
|
||||
|
||||
val currentPlaybackRate = (if (_isCasting) {
|
||||
StateCastingDispatcher.getActiveDeviceSpeed()
|
||||
StateCasting.instance.activeDevice?.speed
|
||||
} else _player.getPlaybackRate()) ?: 1.0
|
||||
_overlay_quality_selector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }?.let {
|
||||
(it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString())
|
||||
|
@ -2414,9 +2353,9 @@ class VideoDetailView : ConstraintLayout {
|
|||
?.distinct()
|
||||
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
||||
|
||||
val canSetSpeed = !_isCasting || StateCastingDispatcher.canActiveDeviceSetSpeed();
|
||||
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed() ?: false
|
||||
val currentPlaybackRate = if (_isCasting) {
|
||||
StateCastingDispatcher.getActiveDeviceSpeed()
|
||||
StateCasting.instance.activeDevice?.speed
|
||||
} else {
|
||||
_player.getPlaybackRate()
|
||||
}
|
||||
|
@ -2434,7 +2373,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
setButtons(playbackLabels, String.format(Locale.US, format, currentPlaybackRate));
|
||||
onClick.subscribe { v ->
|
||||
val currentPlaybackSpeed = if (_isCasting) {
|
||||
StateCastingDispatcher.getActiveDeviceSpeed()
|
||||
StateCasting.instance.activeDevice?.speed
|
||||
} else _player.getPlaybackRate();
|
||||
var playbackSpeedString = v;
|
||||
val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep();
|
||||
|
@ -2443,9 +2382,11 @@ class VideoDetailView : ConstraintLayout {
|
|||
else if(v == "-")
|
||||
playbackSpeedString = String.format(Locale.US, "%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString();
|
||||
val newPlaybackSpeed = playbackSpeedString.toDouble();
|
||||
if (_isCasting && StateCastingDispatcher.canActiveDeviceSetSpeed()) {
|
||||
if (_isCasting && StateCasting.instance.activeDevice?.canSetSpeed() ?: false) {
|
||||
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
||||
StateCastingDispatcher.activeDeviceSetSpeed(newPlaybackSpeed)
|
||||
try {
|
||||
StateCasting.instance.activeDevice?.changeSpeed(newPlaybackSpeed)
|
||||
} catch (_: Throwable) {}
|
||||
setSelected(playbackSpeedString);
|
||||
} else {
|
||||
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
||||
|
@ -2561,7 +2502,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
//Handlers
|
||||
private fun handlePlay() {
|
||||
Logger.i(TAG, "handlePlay")
|
||||
if (!StateCastingDispatcher.resumeVideo()) {
|
||||
if (!StateCasting.instance.resumeVideo()) {
|
||||
_player.play()
|
||||
}
|
||||
|
||||
|
@ -2577,19 +2518,19 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
private fun handlePause() {
|
||||
Logger.i(TAG, "handlePause")
|
||||
if (!StateCastingDispatcher.pauseVideo()) {
|
||||
if (!StateCasting.instance.pauseVideo()) {
|
||||
_player.pause()
|
||||
}
|
||||
}
|
||||
private fun handleSeek(ms: Long) {
|
||||
Logger.i(TAG, "handleSeek(ms=$ms)")
|
||||
if (!StateCastingDispatcher.videoSeekTo(ms.toDouble() / 1000.0)) {
|
||||
if (!StateCasting.instance.videoSeekTo(ms.toDouble() / 1000.0)) {
|
||||
_player.seekTo(ms)
|
||||
}
|
||||
}
|
||||
private fun handleStop() {
|
||||
Logger.i(TAG, "handleStop")
|
||||
if (!StateCastingDispatcher.stopVideo()) {
|
||||
if (!StateCasting.instance.stopVideo()) {
|
||||
_player.stop()
|
||||
}
|
||||
}
|
||||
|
@ -2597,7 +2538,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
private fun handlePlayChanged(playing: Boolean) {
|
||||
Logger.i(TAG, "handlePlayChanged(playing=$playing)")
|
||||
|
||||
if (StateCastingDispatcher.isCasting()) {
|
||||
if (StateCasting.instance.isCasting) {
|
||||
_cast.setIsPlaying(playing);
|
||||
} else {
|
||||
StatePlayer.instance.updateMediaSession( null);
|
||||
|
@ -2639,9 +2580,9 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
if (StateCastingDispatcher.isConnected()) {
|
||||
val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: 0.0
|
||||
val speed = StateCastingDispatcher.getActiveDeviceSpeed() ?: 1.0
|
||||
if (StateCasting.instance.activeDevice != null) {
|
||||
val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0
|
||||
val speed = StateCasting.instance.activeDevice?.speed ?: 1.0
|
||||
castIfAvailable(
|
||||
context.contentResolver,
|
||||
video,
|
||||
|
@ -2670,9 +2611,9 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
if (StateCastingDispatcher.isConnected()) {
|
||||
val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: 0.0
|
||||
val speed = StateCastingDispatcher.getActiveDeviceSpeed() ?: 1.0
|
||||
if (StateCasting.instance.activeDevice != null) {
|
||||
val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0
|
||||
val speed = StateCasting.instance.activeDevice?.speed ?: 1.0
|
||||
castIfAvailable(
|
||||
context.contentResolver,
|
||||
video,
|
||||
|
@ -2702,9 +2643,9 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
if (StateCastingDispatcher.isConnected()) {
|
||||
val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: 0.0
|
||||
val speed = StateCastingDispatcher.getActiveDeviceSpeed() ?: 1.0
|
||||
if (StateCasting.instance.activeDevice != null) {
|
||||
val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0
|
||||
val speed = StateCasting.instance.activeDevice?.speed ?: 1.0
|
||||
castIfAvailable(
|
||||
context.contentResolver,
|
||||
video,
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -33,11 +33,11 @@ import com.futo.platformplayer.activities.SettingsActivity.Companion.settingsAct
|
|||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.background.BackgroundWorker
|
||||
import com.futo.platformplayer.casting.OldStateCasting
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.HomeFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
import com.futo.platformplayer.logging.AndroidLogConsumer
|
||||
|
@ -760,11 +760,7 @@ class StateApp {
|
|||
_connectivityManager?.unregisterNetworkCallback(_connectivityEvents);
|
||||
|
||||
StatePlayer.instance.closeMediaSession();
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.stop()
|
||||
} else {
|
||||
StateCasting.instance.stop()
|
||||
}
|
||||
StateCasting.instance.stop()
|
||||
StateSync.instance.stop();
|
||||
StatePlayer.dispose();
|
||||
Companion.dispose();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
onConnect.emit(dev)
|
||||
} else {
|
||||
try {
|
||||
view.context?.let { UIDialogs.toast(it, "Device not ready, may be offline") }
|
||||
} catch (e: Throwable) {
|
||||
//Ignored
|
||||
}
|
||||
try {
|
||||
if (dev.isReady) {
|
||||
StateCasting.instance.activeDevice?.stopPlayback()
|
||||
StateCasting.instance.connectDevice(dev)
|
||||
onConnect.emit(dev)
|
||||
} else {
|
||||
view.context?.let {
|
||||
UIDialogs.toast(it, "Device not ready, may be offline")
|
||||
}
|
||||
}
|
||||
is GenericCastingDevice.Experimental -> {
|
||||
if (dev.handle.device.isReady()) {
|
||||
// NOTE: we assume experimental casting is used
|
||||
try {
|
||||
ExpStateCasting.instance.activeDevice?.device?.stopPlayback()
|
||||
ExpStateCasting.instance.activeDevice?.device?.disconnect()
|
||||
} catch (e: Throwable) {
|
||||
//Ignored
|
||||
}
|
||||
ExpStateCasting.instance.connectDevice(dev.handle)
|
||||
onConnect.emit(dev)
|
||||
} else {
|
||||
try {
|
||||
view.context?.let { UIDialogs.toast(it, "Device not ready, may be offline") }
|
||||
} catch (e: Throwable) {
|
||||
//Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,122 +77,69 @@ class DeviceViewHolder : ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
// fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
||||
|
||||
fun bind(d: GenericCastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
||||
when (d) {
|
||||
is GenericCastingDevice.Normal -> {
|
||||
if (d.device is ChromecastCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||
_textType.text = "Chromecast";
|
||||
} else if (d.device is AirPlayCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||
_textType.text = "AirPlay";
|
||||
} else if (d.device is FCastCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
||||
_textType.text = "FCast";
|
||||
}
|
||||
|
||||
_textName.text = d.device.name;
|
||||
_imageOnline.visibility = if (isOnlineDevice && d.device.isReady) View.VISIBLE else View.GONE
|
||||
|
||||
if (!d.device.isReady) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.VISIBLE;
|
||||
_imagePin.visibility = View.GONE;
|
||||
} else {
|
||||
_textNotReady.visibility = View.GONE;
|
||||
|
||||
val dev = StateCasting.instance.activeDevice;
|
||||
if (dev == d.device) {
|
||||
if (dev.connectionState == CastConnectionState.CONNECTED) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.GONE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
} else {
|
||||
_imageLoader.visibility = View.VISIBLE;
|
||||
_textNotReady.visibility = View.GONE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
}
|
||||
} else {
|
||||
if (d.device.isReady) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.GONE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
} else {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.VISIBLE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
_imagePin.setImageResource(if (isPinnedDevice) R.drawable.keep_24px else R.drawable.ic_pin)
|
||||
|
||||
if (_imageLoader.isVisible) {
|
||||
_animatableLoader?.start();
|
||||
} else {
|
||||
_animatableLoader?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
device = d;
|
||||
fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
||||
when (d.protocolType) {
|
||||
CastProtocolType.CHROMECAST -> {
|
||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||
_textType.text = "Chromecast";
|
||||
}
|
||||
is GenericCastingDevice.Experimental -> {
|
||||
when (d.handle.device.castingProtocol()) {
|
||||
ProtocolType.CHROMECAST -> {
|
||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||
_textType.text = "Chromecast";
|
||||
}
|
||||
ProtocolType.F_CAST -> {
|
||||
_imageDevice.setImageResource(R.drawable.ic_exp_fc);
|
||||
_textType.text = "FCast";
|
||||
}
|
||||
}
|
||||
|
||||
_textName.text = d.handle.device.name();
|
||||
_imageOnline.visibility = if (isOnlineDevice && d.handle.device.isReady()) View.VISIBLE else View.GONE
|
||||
|
||||
if (!d.handle.device.isReady()) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.VISIBLE;
|
||||
_imagePin.visibility = View.GONE;
|
||||
CastProtocolType.AIRPLAY -> {
|
||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||
_textType.text = "AirPlay";
|
||||
}
|
||||
CastProtocolType.FCAST -> {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_exp_fc)
|
||||
} else {
|
||||
_textNotReady.visibility = View.GONE;
|
||||
|
||||
val dev = ExpStateCasting.instance.activeDevice;
|
||||
if (dev == d.handle) {
|
||||
if (dev.connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.GONE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
} else {
|
||||
_imageLoader.visibility = View.VISIBLE;
|
||||
_textNotReady.visibility = View.GONE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
}
|
||||
} else {
|
||||
if (d.handle.device.isReady()) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.GONE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
} else {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.VISIBLE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
_imagePin.setImageResource(if (isPinnedDevice) R.drawable.keep_24px else R.drawable.ic_pin)
|
||||
|
||||
if (_imageLoader.isVisible) {
|
||||
_animatableLoader?.start();
|
||||
} else {
|
||||
_animatableLoader?.stop();
|
||||
}
|
||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
||||
}
|
||||
|
||||
device = d;
|
||||
_textType.text = "FCast";
|
||||
}
|
||||
}
|
||||
|
||||
_textName.text = d.name;
|
||||
_imageOnline.visibility = if (isOnlineDevice && d.isReady) View.VISIBLE else View.GONE
|
||||
|
||||
|
||||
if (!d.isReady) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.VISIBLE;
|
||||
_imagePin.visibility = View.GONE;
|
||||
} else {
|
||||
_textNotReady.visibility = View.GONE;
|
||||
|
||||
val dev = StateCasting.instance.activeDevice;
|
||||
if (dev == d) {
|
||||
if (dev.connectionState == CastConnectionState.CONNECTED) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.GONE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
} else {
|
||||
_imageLoader.visibility = View.VISIBLE;
|
||||
_textNotReady.visibility = View.GONE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
}
|
||||
} else {
|
||||
if (d.isReady) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.GONE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
} else {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.VISIBLE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
_imagePin.setImageResource(if (isPinnedDevice) R.drawable.keep_24px else R.drawable.ic_pin)
|
||||
|
||||
if (_imageLoader.isVisible) {
|
||||
_animatableLoader?.start();
|
||||
} else {
|
||||
_animatableLoader?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
device = d;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
};
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
|
||||
updateCastState()
|
||||
}
|
||||
|
||||
updateCastState();
|
||||
|
@ -45,47 +34,26 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
|||
|
||||
private fun updateCastState() {
|
||||
val c = context ?: return;
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
val d = ExpStateCasting.instance.activeDevice;
|
||||
|
||||
val activeColor = ContextCompat.getColor(c, R.color.colorPrimary);
|
||||
val connectingColor = ContextCompat.getColor(c, R.color.gray_c3);
|
||||
val inactiveColor = ContextCompat.getColor(c, R.color.white);
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
|
||||
if (d != null) {
|
||||
when (d.connectionState) {
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> setColorFilter(activeColor)
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTING -> setColorFilter(connectingColor)
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> setColorFilter(activeColor)
|
||||
}
|
||||
} else {
|
||||
setColorFilter(inactiveColor);
|
||||
val activeColor = ContextCompat.getColor(c, R.color.colorPrimary);
|
||||
val connectingColor = ContextCompat.getColor(c, R.color.gray_c3);
|
||||
val inactiveColor = ContextCompat.getColor(c, R.color.white);
|
||||
|
||||
if (d != null) {
|
||||
when (d.connectionState) {
|
||||
DISCONNECTED -> setColorFilter(activeColor)
|
||||
CONNECTING -> setColorFilter(connectingColor)
|
||||
CONNECTED -> setColorFilter(activeColor)
|
||||
}
|
||||
} else {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
|
||||
val activeColor = ContextCompat.getColor(c, R.color.colorPrimary);
|
||||
val connectingColor = ContextCompat.getColor(c, R.color.gray_c3);
|
||||
val inactiveColor = ContextCompat.getColor(c, R.color.white);
|
||||
|
||||
if (d != null) {
|
||||
when (d.connectionState) {
|
||||
CastConnectionState.CONNECTED -> setColorFilter(activeColor)
|
||||
CastConnectionState.CONNECTING -> setColorFilter(connectingColor)
|
||||
CastConnectionState.DISCONNECTED -> setColorFilter(activeColor)
|
||||
}
|
||||
} else {
|
||||
setColorFilter(inactiveColor);
|
||||
}
|
||||
setColorFilter(inactiveColor);
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
setOnClickListener(null);
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
} else {
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
}
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
}
|
||||
}
|
|
@ -21,17 +21,12 @@ import com.futo.platformplayer.R
|
|||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
||||
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.experimental_casting.StateCastingDispatcher
|
||||
import com.futo.platformplayer.formatDuration
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
||||
import com.futo.platformplayer.views.behavior.GestureControlView
|
||||
|
@ -39,9 +34,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.fcast.sender_sdk.DeviceFeature
|
||||
|
||||
class CastView : ConstraintLayout {
|
||||
private val _thumbnail: ImageView;
|
||||
|
@ -100,51 +93,40 @@ class CastView : ConstraintLayout {
|
|||
_gestureControlView.fullScreenGestureEnabled = false
|
||||
_gestureControlView.setupTouchArea();
|
||||
_gestureControlView.onSpeedHoldStart.subscribe {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
val d = ExpStateCasting.instance.activeDevice ?: return@subscribe;
|
||||
_speedHoldWasPlaying = d.isPlaying
|
||||
_speedHoldPrevRate = d.speed
|
||||
if (d.device.supportsFeature(DeviceFeature.SET_SPEED)) {
|
||||
try {
|
||||
d.device.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
||||
} catch (e: Throwable) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
val d = StateCasting.instance.activeDevice ?: return@subscribe
|
||||
_speedHoldWasPlaying = d.isPlaying
|
||||
_speedHoldPrevRate = d.speed
|
||||
if (d.canSetSpeed()) {
|
||||
try {
|
||||
d.device.resumePlayback()
|
||||
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
||||
} catch (e: Throwable) {
|
||||
// Ignored
|
||||
}
|
||||
} else {
|
||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||
_speedHoldWasPlaying = d.isPlaying
|
||||
_speedHoldPrevRate = d.speed
|
||||
if (d.canSetSpeed) {
|
||||
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
||||
}
|
||||
d.resumeVideo()
|
||||
}
|
||||
try {
|
||||
d.resumePlayback()
|
||||
} catch (e: Throwable) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
_gestureControlView.onSpeedHoldEnd.subscribe {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
val d = ExpStateCasting.instance.activeDevice ?: return@subscribe;
|
||||
if (!_speedHoldWasPlaying) {
|
||||
d.device.resumePlayback()
|
||||
}
|
||||
d.device.changeSpeed(_speedHoldPrevRate)
|
||||
} else {
|
||||
try {
|
||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||
if (!_speedHoldWasPlaying) {
|
||||
d.pauseVideo()
|
||||
d.resumePlayback()
|
||||
}
|
||||
d.changeSpeed(_speedHoldPrevRate)
|
||||
} catch (e: Throwable) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
_gestureControlView.onSeek.subscribe {
|
||||
val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: return@subscribe
|
||||
StateCastingDispatcher.videoSeekTo(expectedCurrentTime + it / 1000)
|
||||
try {
|
||||
val d = StateCasting.instance.activeDevice ?: return@subscribe
|
||||
val expectedCurrentTime = d.expectedCurrentTime
|
||||
d.seekTo(expectedCurrentTime + it / 1000)
|
||||
} catch (_: Throwable) { }
|
||||
};
|
||||
|
||||
_buttonLoop.setOnClickListener {
|
||||
|
@ -155,25 +137,35 @@ class CastView : ConstraintLayout {
|
|||
|
||||
_timeBar.addListener(object : TimeBar.OnScrubListener {
|
||||
override fun onScrubStart(timeBar: TimeBar, position: Long) {
|
||||
StateCastingDispatcher.videoSeekTo(position.toDouble())
|
||||
try {
|
||||
StateCasting.instance.activeDevice?.seekTo(position.toDouble())
|
||||
} catch (_: Throwable) { }
|
||||
}
|
||||
|
||||
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
||||
StateCastingDispatcher.videoSeekTo(position.toDouble())
|
||||
try {
|
||||
StateCasting.instance.activeDevice?.seekTo(position.toDouble())
|
||||
} catch (_: Throwable) { }
|
||||
}
|
||||
|
||||
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
||||
StateCastingDispatcher.videoSeekTo(position.toDouble())
|
||||
try {
|
||||
StateCasting.instance.activeDevice?.seekTo(position.toDouble())
|
||||
} catch (_: Throwable) { }
|
||||
}
|
||||
});
|
||||
|
||||
_buttonMinimize.setOnClickListener { onMinimizeClick.emit(); };
|
||||
_buttonSettings.setOnClickListener { onSettingsClick.emit(); };
|
||||
_buttonPlay.setOnClickListener {
|
||||
StateCastingDispatcher.resumeVideo()
|
||||
try {
|
||||
StateCasting.instance.activeDevice?.resumePlayback()
|
||||
} catch (_: Throwable) { }
|
||||
}
|
||||
_buttonPause.setOnClickListener {
|
||||
StateCastingDispatcher.pauseVideo()
|
||||
try {
|
||||
StateCasting.instance.activeDevice?.pausePlayback()
|
||||
} catch (_: Throwable) { }
|
||||
}
|
||||
|
||||
if (!isInEditMode) {
|
||||
|
@ -257,25 +249,9 @@ class CastView : ConstraintLayout {
|
|||
stopTimeJob()
|
||||
|
||||
if(isPlaying) {
|
||||
// NOTE: the experimental implementation polls automatically
|
||||
if (!Settings.instance.casting.experimentalCasting) {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d is AirPlayCastingDevice || d is ChromecastCastingDevice) {
|
||||
_updateTimeJob = _scope.launch {
|
||||
while (true) {
|
||||
val device = StateCasting.instance.activeDevice;
|
||||
if (device == null || !device.isPlaying) {
|
||||
break;
|
||||
}
|
||||
|
||||
delay(1000);
|
||||
val time_ms = (device.expectedCurrentTime * 1000.0).toLong()
|
||||
setTime(time_ms);
|
||||
onTimeJobTimeChanged_s.emit(device.expectedCurrentTime.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StateCasting.instance.startUpdateTimeJob(
|
||||
onTimeJobTimeChanged_s
|
||||
) { setTime(it) }
|
||||
|
||||
if (!_inPictureInPicture) {
|
||||
_buttonPause.visibility = View.VISIBLE;
|
||||
|
@ -287,7 +263,7 @@ class CastView : ConstraintLayout {
|
|||
_buttonPlay.visibility = View.VISIBLE;
|
||||
}
|
||||
|
||||
val position = StateCastingDispatcher.getExpectedCurrentTime()?.times(1000.0)?.toLong()
|
||||
val position = StateCasting.instance.activeDevice?.expectedCurrentTime?.times(1000.0)?.toLong()
|
||||
if(StatePlayer.instance.hasMediaSession()) {
|
||||
StatePlayer.instance.updateMediaSession(null);
|
||||
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), (position ?: 0));
|
||||
|
@ -351,10 +327,10 @@ class CastView : ConstraintLayout {
|
|||
}
|
||||
|
||||
private fun getPlaybackStateCompat(): Int {
|
||||
if (!StateCastingDispatcher.isConnected()) {
|
||||
if (StateCasting.instance.activeDevice?.connectionState != CastConnectionState.CONNECTED) {
|
||||
return PlaybackState.STATE_NONE
|
||||
}
|
||||
return when(StateCastingDispatcher.isPlaying()) {
|
||||
return when(StateCasting.instance.activeDevice?.isPlaying) {
|
||||
true -> PlaybackStateCompat.STATE_PLAYING;
|
||||
else -> PlaybackStateCompat.STATE_PAUSED;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue