mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-09-13 04:52:28 +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.Color
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.Layout
|
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
|
@ -22,6 +21,7 @@ import androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
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.casting.StateCasting
|
||||||
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
import com.futo.platformplayer.dialogs.AutoUpdateDialog
|
||||||
import com.futo.platformplayer.dialogs.AutomaticBackupDialog
|
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.PluginUpdateDialog
|
||||||
import com.futo.platformplayer.dialogs.ProgressDialog
|
import com.futo.platformplayer.dialogs.ProgressDialog
|
||||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
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.MainFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||||
|
@ -438,8 +437,7 @@ class UIDialogs {
|
||||||
|
|
||||||
|
|
||||||
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
|
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
val d = StateCasting.instance.activeDevice
|
||||||
val d = ExpStateCasting.instance.activeDevice;
|
|
||||||
if (d != null) {
|
if (d != null) {
|
||||||
val dialog = ConnectedCastingDialog(context);
|
val dialog = ConnectedCastingDialog(context);
|
||||||
if (context is Activity) {
|
if (context is Activity) {
|
||||||
|
@ -463,32 +461,6 @@ class UIDialogs {
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
val d = StateCasting.instance.activeDevice;
|
|
||||||
if (d != null) {
|
|
||||||
val dialog = ConnectedCastingDialog(context);
|
|
||||||
if (context is Activity) {
|
|
||||||
dialog.setOwnerActivity(context)
|
|
||||||
}
|
|
||||||
registerDialogOpened(dialog);
|
|
||||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
|
||||||
dialog.show();
|
|
||||||
} else {
|
|
||||||
val dialog = ConnectCastingDialog(context);
|
|
||||||
if (context is Activity) {
|
|
||||||
dialog.setOwnerActivity(context)
|
|
||||||
}
|
|
||||||
registerDialogOpened(dialog);
|
|
||||||
val c = context
|
|
||||||
if (c is Activity) {
|
|
||||||
dialog.setOwnerActivity(c);
|
|
||||||
}
|
|
||||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
|
||||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
|
||||||
dialog.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
|
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
|
||||||
|
|
|
@ -42,7 +42,6 @@ import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
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.bottombar.MenuBottomBarFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
||||||
|
@ -118,7 +117,6 @@ import java.util.LinkedList
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
//TODO: Move to dimensions
|
//TODO: Move to dimensions
|
||||||
|
@ -508,12 +506,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
handleIntent(intent);
|
handleIntent(intent);
|
||||||
|
|
||||||
if (Settings.instance.casting.enabled) {
|
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 {
|
StatePlatform.instance.onDevSourceChanged.subscribe {
|
||||||
Logger.i(TAG, "onDevSourceChanged")
|
Logger.i(TAG, "onDevSourceChanged")
|
||||||
|
@ -1051,11 +1045,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
Logger.i(TAG, "handleFCast");
|
Logger.i(TAG, "handleFCast");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
StateCasting.instance.handleUrl(url)
|
||||||
ExpStateCasting.instance.handleUrl(this, url)
|
|
||||||
} else {
|
|
||||||
StateCasting.instance.handleUrl(this, url)
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
||||||
|
|
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class AirPlayCastingDevice : CastingDevice {
|
class AirPlayCastingDevice : OldCastingDevice {
|
||||||
//See for more info: https://nto.github.io/AirPlay
|
//See for more info: https://nto.github.io/AirPlay
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.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.constructs.Event1
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import kotlinx.serialization.KSerializer
|
import org.fcast.sender_sdk.Metadata
|
||||||
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 java.net.InetAddress
|
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 class CastingDevice {
|
||||||
abstract val protocol: CastProtocolType;
|
abstract val isReady: Boolean
|
||||||
abstract val isReady: Boolean;
|
abstract val usedRemoteAddress: InetAddress?
|
||||||
abstract var usedRemoteAddress: InetAddress?;
|
abstract val localAddress: InetAddress?
|
||||||
abstract var localAddress: InetAddress?;
|
abstract val name: String?
|
||||||
abstract val canSetVolume: Boolean;
|
abstract val onConnectionStateChanged: Event1<CastConnectionState>
|
||||||
abstract val canSetSpeed: Boolean;
|
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;
|
@Throws
|
||||||
var isPlaying: Boolean = false
|
abstract fun resumePlayback()
|
||||||
set(value) {
|
|
||||||
val changed = value != field;
|
|
||||||
field = value;
|
|
||||||
if (changed) {
|
|
||||||
onPlayChanged.emit(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private var lastTimeChangeTime_ms: Long = 0
|
@Throws
|
||||||
var time: Double = 0.0
|
abstract fun pausePlayback()
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
|
abstract fun stopPlayback()
|
||||||
time = value
|
|
||||||
lastTimeChangeTime_ms = changeTime_ms
|
|
||||||
onTimeChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastDurationChangeTime_ms: Long = 0
|
@Throws
|
||||||
var duration: Double = 0.0
|
abstract fun seekTo(timeSeconds: Double)
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
|
abstract fun changeVolume(timeSeconds: Double)
|
||||||
duration = value
|
|
||||||
lastDurationChangeTime_ms = changeTime_ms
|
|
||||||
onDurationChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastVolumeChangeTime_ms: Long = 0
|
@Throws
|
||||||
var volume: Double = 1.0
|
abstract fun changeSpeed(speed: Double)
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
|
abstract fun connect()
|
||||||
volume = value
|
|
||||||
lastVolumeChangeTime_ms = changeTime_ms
|
|
||||||
onVolumeChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastSpeedChangeTime_ms: Long = 0
|
@Throws
|
||||||
var speed: Double = 1.0
|
abstract fun disconnect()
|
||||||
private set
|
abstract fun getDeviceInfo(): CastingDeviceInfo
|
||||||
|
abstract fun getAddresses(): List<InetAddress>
|
||||||
|
|
||||||
protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
|
abstract fun loadVideo(
|
||||||
speed = value
|
streamType: String,
|
||||||
lastSpeedChangeTime_ms = changeTime_ms
|
contentType: String,
|
||||||
onSpeedChanged.emit(value)
|
contentId: String,
|
||||||
}
|
resumePosition: Double,
|
||||||
}
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
)
|
||||||
|
|
||||||
val expectedCurrentTime: Double
|
@Throws
|
||||||
get() {
|
abstract fun loadContent(
|
||||||
val diff = if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
|
contentType: String,
|
||||||
return time + diff;
|
content: String,
|
||||||
};
|
resumePosition: Double,
|
||||||
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
|
duration: Double,
|
||||||
set(value) {
|
speed: Double?,
|
||||||
val changed = value != field;
|
metadata: Metadata?
|
||||||
field = value;
|
)
|
||||||
|
|
||||||
if (changed) {
|
abstract fun ensureThreadStarted()
|
||||||
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>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ import javax.net.ssl.SSLSocket
|
||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
class ChromecastCastingDevice : CastingDevice {
|
class ChromecastCastingDevice : OldCastingDevice {
|
||||||
//See for more info: https://developers.google.com/cast/docs/media/messages
|
//See for more info: https://developers.google.com/cast/docs/media/messages
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.CHROMECAST;
|
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.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.Settings
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
||||||
|
@ -25,7 +24,6 @@ import com.futo.platformplayer.toInetAddress
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
@ -34,7 +32,6 @@ import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
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
|
//See for more info: TODO
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.FCAST;
|
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.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.CastProtocolType
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
|
import com.futo.platformplayer.casting.OldStateCasting
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.toInetAddress
|
import com.futo.platformplayer.toInetAddress
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
@ -110,15 +110,11 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
||||||
|
|
||||||
_textError.visibility = View.GONE;
|
_textError.visibility = View.GONE;
|
||||||
val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt());
|
val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt());
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
|
||||||
try {
|
try {
|
||||||
ExpStateCasting.instance.addRememberedDevice(castingDeviceInfo)
|
StateCasting.instance.addRememberedDevice(castingDeviceInfo)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to add remembered device: $e")
|
Logger.e(TAG, "Failed to add remembered device: $e")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
StateCasting.instance.addRememberedDevice(castingDeviceInfo)
|
|
||||||
}
|
|
||||||
performDismiss();
|
performDismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -15,18 +14,14 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.views.adapters.DeviceAdapter
|
import com.futo.platformplayer.views.adapters.DeviceAdapter
|
||||||
import com.futo.platformplayer.views.adapters.DeviceAdapterEntry
|
import com.futo.platformplayer.views.adapters.DeviceAdapterEntry
|
||||||
import com.futo.platformplayer.views.adapters.GenericCastingDevice
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@ -58,33 +53,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
_recyclerDevices.layoutManager = LinearLayoutManager(context);
|
_recyclerDevices.layoutManager = LinearLayoutManager(context);
|
||||||
|
|
||||||
_adapter.onPin.subscribe { d ->
|
_adapter.onPin.subscribe { d ->
|
||||||
when (d) {
|
val isRemembered = _rememberedDevices.contains(d.name)
|
||||||
is GenericCastingDevice.Experimental -> {
|
|
||||||
val isRemembered = _rememberedDevices.contains(d.handle.device.name())
|
|
||||||
val newIsRemembered = !isRemembered
|
val newIsRemembered = !isRemembered
|
||||||
if (newIsRemembered) {
|
if (newIsRemembered) {
|
||||||
ExpStateCasting.instance.addRememberedDevice(d.handle)
|
StateCasting.instance.addRememberedDevice(d)
|
||||||
val name = d.handle.device.name()
|
val name = d.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) {
|
if (name != null) {
|
||||||
_rememberedDevices.add(name)
|
_rememberedDevices.add(name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StateCasting.instance.removeRememberedDevice(d.device)
|
StateCasting.instance.removeRememberedDevice(d)
|
||||||
_rememberedDevices.remove(d.device.name)
|
_rememberedDevices.remove(d.name)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
}
|
}
|
||||||
|
@ -124,60 +103,26 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
|
|
||||||
(_imageLoader.drawable as Animatable?)?.start();
|
(_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) {
|
synchronized(StateCasting.instance.devices) {
|
||||||
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
|
_devices.addAll(StateCasting.instance.devices.values.map { it.name.orEmpty() })
|
||||||
}
|
}
|
||||||
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
||||||
}
|
|
||||||
|
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
|
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
|
||||||
ExpStateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
|
||||||
_devices.add(d.name())
|
|
||||||
updateUnifiedList()
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpStateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
|
||||||
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name() == d.device.name() }
|
|
||||||
if (index != -1) {
|
|
||||||
val dev = GenericCastingDevice.Experimental(d)
|
|
||||||
_unifiedDevices[index] = DeviceAdapterEntry(dev, _unifiedDevices[index].isPinnedDevice, _unifiedDevices[index].isOnlineDevice)
|
|
||||||
_adapter.notifyItemChanged(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpStateCasting.instance.onDeviceRemoved.subscribe(this) { deviceName ->
|
|
||||||
_devices.remove(deviceName)
|
|
||||||
updateUnifiedList()
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
|
||||||
if (connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED) {
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
||||||
val name = d.name
|
val name = d.name
|
||||||
if (name != null)
|
if (name != null) {
|
||||||
_devices.add(name)
|
_devices.add(name)
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
||||||
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name() == d.name }
|
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name == d.name }
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
_unifiedDevices[index] = DeviceAdapterEntry(
|
_unifiedDevices[index] = DeviceAdapterEntry(
|
||||||
GenericCastingDevice.Normal(d),
|
d,
|
||||||
_unifiedDevices[index].isPinnedDevice,
|
_unifiedDevices[index].isPinnedDevice,
|
||||||
_unifiedDevices[index].isOnlineDevice
|
_unifiedDevices[index].isOnlineDevice
|
||||||
)
|
)
|
||||||
|
@ -185,8 +130,8 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateCasting.instance.onDeviceRemoved.subscribe(this) { d ->
|
StateCasting.instance.onDeviceRemoved.subscribe(this) { deviceName ->
|
||||||
_devices.remove(d.name)
|
_devices.remove(deviceName.name)
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,23 +143,15 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun dismiss() {
|
override fun dismiss() {
|
||||||
super.dismiss()
|
super.dismiss()
|
||||||
(_imageLoader.drawable as Animatable?)?.stop()
|
(_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.onDeviceAdded.remove(this)
|
||||||
StateCasting.instance.onDeviceChanged.remove(this)
|
StateCasting.instance.onDeviceChanged.remove(this)
|
||||||
StateCasting.instance.onDeviceRemoved.remove(this)
|
StateCasting.instance.onDeviceRemoved.remove(this)
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this)
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateUnifiedList() {
|
private fun updateUnifiedList() {
|
||||||
val oldList = ArrayList(_unifiedDevices)
|
val oldList = ArrayList(_unifiedDevices)
|
||||||
|
@ -226,16 +163,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldList[oldItemPosition]
|
val oldItem = oldList[oldItemPosition]
|
||||||
val newItem = newList[newItemPosition]
|
val newItem = newList[newItemPosition]
|
||||||
return oldItem.castingDevice.name() == newItem.castingDevice.name()
|
return oldItem.castingDevice.name == newItem.castingDevice.name
|
||||||
&& oldItem.castingDevice.isReady() == newItem.castingDevice.isReady()
|
&& oldItem.castingDevice.isReady == newItem.castingDevice.isReady
|
||||||
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
|
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
|
||||||
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
|
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldList[oldItemPosition]
|
val oldItem = oldList[oldItemPosition]
|
||||||
val newItem = newList[newItemPosition]
|
val newItem = newList[newItemPosition]
|
||||||
return oldItem.castingDevice.name() == newItem.castingDevice.name()
|
return oldItem.castingDevice.name == newItem.castingDevice.name
|
||||||
&& oldItem.castingDevice.isReady() == newItem.castingDevice.isReady()
|
&& oldItem.castingDevice.isReady == newItem.castingDevice.isReady
|
||||||
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
|
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
|
||||||
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
|
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
|
||||||
}
|
}
|
||||||
|
@ -252,45 +190,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
private fun buildUnifiedList(): List<DeviceAdapterEntry> {
|
private fun buildUnifiedList(): List<DeviceAdapterEntry> {
|
||||||
val unifiedList = mutableListOf<DeviceAdapterEntry>()
|
val unifiedList = mutableListOf<DeviceAdapterEntry>()
|
||||||
|
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
|
||||||
val onlineDevices = ExpStateCasting.instance.devices.values.associateBy { it.device.name() }
|
|
||||||
val rememberedDevices = ExpStateCasting.instance.getRememberedCastingDevices().associateBy { it.device.name() }
|
|
||||||
|
|
||||||
val intersectionNames = _devices.intersect(_rememberedDevices)
|
|
||||||
for (name in intersectionNames) {
|
|
||||||
onlineDevices[name]?.let {
|
|
||||||
unifiedList.add(DeviceAdapterEntry(
|
|
||||||
GenericCastingDevice.Experimental(it), true, true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onlineOnlyNames = _devices - _rememberedDevices
|
|
||||||
for (name in onlineOnlyNames) {
|
|
||||||
onlineDevices[name]?.let {
|
|
||||||
unifiedList.add(DeviceAdapterEntry(
|
|
||||||
GenericCastingDevice.Experimental(it), false, true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val rememberedOnlyNames = _rememberedDevices - _devices
|
|
||||||
for (name in rememberedOnlyNames) {
|
|
||||||
rememberedDevices[name]?.let {
|
|
||||||
unifiedList.add(DeviceAdapterEntry(
|
|
||||||
GenericCastingDevice.Experimental(it), true, false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val onlineDevices = StateCasting.instance.devices.values.associateBy { it.name }
|
val onlineDevices = StateCasting.instance.devices.values.associateBy { it.name }
|
||||||
val rememberedDevices = StateCasting.instance.getRememberedCastingDevices().associateBy { it.name }
|
val rememberedDevices =
|
||||||
|
StateCasting.instance.getRememberedCastingDevices().associateBy { it.name }
|
||||||
|
|
||||||
val intersectionNames = _devices.intersect(_rememberedDevices)
|
val intersectionNames = _devices.intersect(_rememberedDevices)
|
||||||
for (name in intersectionNames) {
|
for (name in intersectionNames) {
|
||||||
onlineDevices[name]?.let {
|
onlineDevices[name]?.let {
|
||||||
unifiedList.add(DeviceAdapterEntry(
|
unifiedList.add(
|
||||||
GenericCastingDevice.Normal(it), true, true)
|
DeviceAdapterEntry(
|
||||||
|
it, true, true
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -298,18 +208,22 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
val onlineOnlyNames = _devices - _rememberedDevices
|
val onlineOnlyNames = _devices - _rememberedDevices
|
||||||
for (name in onlineOnlyNames) {
|
for (name in onlineOnlyNames) {
|
||||||
onlineDevices[name]?.let {
|
onlineDevices[name]?.let {
|
||||||
unifiedList.add(DeviceAdapterEntry(
|
unifiedList.add(
|
||||||
GenericCastingDevice.Normal( it), false, true))
|
DeviceAdapterEntry(
|
||||||
|
it, false, true
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val rememberedOnlyNames = _rememberedDevices - _devices
|
val rememberedOnlyNames = _rememberedDevices - _devices
|
||||||
for (name in rememberedOnlyNames) {
|
for (name in rememberedOnlyNames) {
|
||||||
rememberedDevices[name]?.let {
|
rememberedDevices[name]?.let {
|
||||||
unifiedList.add(DeviceAdapterEntry(
|
unifiedList.add(
|
||||||
GenericCastingDevice.Normal(it), true, false)
|
DeviceAdapterEntry(
|
||||||
|
it, true, false
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,16 +16,15 @@ import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
import com.futo.platformplayer.casting.CastingDevice
|
||||||
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
||||||
import com.futo.platformplayer.casting.FCastCastingDevice
|
import com.futo.platformplayer.casting.FCastCastingDevice
|
||||||
|
import com.futo.platformplayer.casting.OldStateCasting
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
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.fragment.mainactivity.main.VideoDetailFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
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
|
||||||
import com.google.android.material.slider.Slider.OnChangeListener
|
import com.google.android.material.slider.Slider.OnChangeListener
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -51,7 +50,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
private lateinit var _buttonStop: ImageButton;
|
private lateinit var _buttonStop: ImageButton;
|
||||||
private lateinit var _buttonNext: ImageButton;
|
private lateinit var _buttonNext: ImageButton;
|
||||||
|
|
||||||
private var _device: GenericCastingDevice? = null;
|
private var _device: CastingDevice? = null;
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -75,18 +74,24 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
|
|
||||||
_buttonPlay = findViewById(R.id.button_play);
|
_buttonPlay = findViewById(R.id.button_play);
|
||||||
_buttonPlay.setOnClickListener {
|
_buttonPlay.setOnClickListener {
|
||||||
StateCastingDispatcher.resumeVideo()
|
try {
|
||||||
|
StateCasting.instance.activeDevice?.resumePlayback()
|
||||||
|
} catch (_: Throwable) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonPause = findViewById(R.id.button_pause);
|
_buttonPause = findViewById(R.id.button_pause);
|
||||||
_buttonPause.setOnClickListener {
|
_buttonPause.setOnClickListener {
|
||||||
StateCastingDispatcher.pauseVideo()
|
try {
|
||||||
|
StateCasting.instance.activeDevice?.pausePlayback()
|
||||||
|
} catch (_: Throwable) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonStop = findViewById(R.id.button_stop);
|
_buttonStop = findViewById(R.id.button_stop);
|
||||||
_buttonStop.setOnClickListener {
|
_buttonStop.setOnClickListener {
|
||||||
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
||||||
StateCastingDispatcher.stopVideo()
|
try {
|
||||||
|
StateCasting.instance.activeDevice?.stopPlayback()
|
||||||
|
} catch (_: Throwable) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonNext = findViewById(R.id.button_next);
|
_buttonNext = findViewById(R.id.button_next);
|
||||||
|
@ -96,16 +101,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
|
|
||||||
_buttonClose.setOnClickListener { dismiss(); };
|
_buttonClose.setOnClickListener { dismiss(); };
|
||||||
_buttonDisconnect.setOnClickListener {
|
_buttonDisconnect.setOnClickListener {
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
|
||||||
try {
|
try {
|
||||||
ExpStateCasting.instance.activeDevice?.device?.stopPlayback()
|
StateCasting.instance.activeDevice?.disconnect()
|
||||||
ExpStateCasting.instance.activeDevice?.device?.disconnect()
|
} catch (_: Throwable) {}
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Ignored
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
StateCasting.instance.activeDevice?.stopCasting();
|
|
||||||
}
|
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -114,7 +112,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
return@OnChangeListener
|
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
|
//TODO: Check if volume slider is properly hidden in all cases
|
||||||
|
@ -123,7 +123,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
return@OnChangeListener
|
return@OnChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
StateCastingDispatcher.changeVolume(value.toDouble())
|
try {
|
||||||
|
StateCasting.instance.activeDevice?.changeVolume(value.toDouble())
|
||||||
|
} catch (_: Throwable) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -134,64 +136,34 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
super.show();
|
super.show();
|
||||||
Logger.i(TAG, "Dialog shown.");
|
Logger.i(TAG, "Dialog shown.");
|
||||||
|
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this)
|
||||||
ExpStateCasting.instance.onActiveDeviceVolumeChanged.remove(this)
|
StateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
||||||
ExpStateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
|
||||||
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo)
|
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
ExpStateCasting.instance.onActiveDeviceTimeChanged.remove(this)
|
StateCasting.instance.onActiveDeviceTimeChanged.remove(this)
|
||||||
ExpStateCasting.instance.onActiveDeviceTimeChanged.subscribe {
|
StateCasting.instance.onActiveDeviceTimeChanged.subscribe {
|
||||||
_sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo)
|
_sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
ExpStateCasting.instance.onActiveDeviceDurationChanged.remove(this)
|
StateCasting.instance.onActiveDeviceDurationChanged.remove(this)
|
||||||
ExpStateCasting.instance.onActiveDeviceDurationChanged.subscribe {
|
StateCasting.instance.onActiveDeviceDurationChanged.subscribe {
|
||||||
val dur = it.toFloat().coerceAtLeast(1.0f)
|
val dur = it.toFloat().coerceAtLeast(1.0f)
|
||||||
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur)
|
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||||
_sliderPosition.valueTo = 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
|
val ad = StateCasting.instance.activeDevice
|
||||||
if (ad != null) {
|
if (ad != null) {
|
||||||
_device = GenericCastingDevice.Normal(ad)
|
_device = ad
|
||||||
}
|
}
|
||||||
val isConnected = ad != null && ad.connectionState == CastConnectionState.CONNECTED;
|
val isConnected = ad != null && ad.connectionState == CastConnectionState.CONNECTED
|
||||||
setLoading(!isConnected);
|
setLoading(!isConnected)
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { setLoading(connectionState != CastConnectionState.CONNECTED); };
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||||
updateDevice()
|
setLoading(connectionState != CastConnectionState.CONNECTED)
|
||||||
}
|
}
|
||||||
|
updateDevice()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDevice();
|
updateDevice();
|
||||||
|
@ -199,81 +171,37 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
|
|
||||||
override fun dismiss() {
|
override fun dismiss() {
|
||||||
super.dismiss();
|
super.dismiss();
|
||||||
|
|
||||||
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this);
|
StateCasting.instance.onActiveDeviceVolumeChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
|
StateCasting.instance.onActiveDeviceDurationChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.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;
|
_device = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDevice() {
|
private fun updateDevice() {
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
|
||||||
val d = ExpStateCasting.instance.activeDevice ?: return;
|
|
||||||
|
|
||||||
when (d.device.castingProtocol()) {
|
|
||||||
ProtocolType.CHROMECAST -> {
|
|
||||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
|
||||||
_textType.text = "Chromecast";
|
|
||||||
}
|
|
||||||
ProtocolType.F_CAST -> {
|
|
||||||
_imageDevice.setImageResource(R.drawable.ic_exp_fc);
|
|
||||||
_textType.text = "FCast";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_textName.text = d.device.name();
|
|
||||||
_sliderPosition.valueFrom = 0.0f;
|
|
||||||
_sliderVolume.valueFrom = 0.0f;
|
|
||||||
_sliderVolume.value = d.volume.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
|
||||||
|
|
||||||
val dur = d.duration.toFloat().coerceAtLeast(1.0f)
|
|
||||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
|
||||||
_sliderPosition.valueTo = dur
|
|
||||||
|
|
||||||
if (d.device.supportsFeature(DeviceFeature.SET_VOLUME)) {
|
|
||||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
|
||||||
_layoutVolumeFixed.visibility = View.GONE;
|
|
||||||
} else {
|
|
||||||
_layoutVolumeAdjustable.visibility = View.GONE;
|
|
||||||
_layoutVolumeFixed.visibility = View.VISIBLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
val interactiveControls = listOf(
|
|
||||||
_sliderPosition,
|
|
||||||
_sliderVolume,
|
|
||||||
_buttonPrevious,
|
|
||||||
_buttonPlay,
|
|
||||||
_buttonPause,
|
|
||||||
_buttonStop,
|
|
||||||
_buttonNext
|
|
||||||
)
|
|
||||||
|
|
||||||
when (d.connectionState) {
|
|
||||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> {
|
|
||||||
enableControls(interactiveControls)
|
|
||||||
}
|
|
||||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTING,
|
|
||||||
com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> {
|
|
||||||
disableControls(interactiveControls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val d = StateCasting.instance.activeDevice ?: return;
|
val d = StateCasting.instance.activeDevice ?: return;
|
||||||
|
|
||||||
if (d is ChromecastCastingDevice) {
|
when (d.protocolType) {
|
||||||
|
CastProtocolType.CHROMECAST -> {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||||
_textType.text = "Chromecast";
|
_textType.text = "Chromecast";
|
||||||
} else if (d is AirPlayCastingDevice) {
|
}
|
||||||
|
CastProtocolType.AIRPLAY -> {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||||
_textType.text = "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";
|
_textType.text = "FCast";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_textName.text = d.name;
|
_textName.text = d.name;
|
||||||
_sliderPosition.valueFrom = 0.0f;
|
_sliderPosition.valueFrom = 0.0f;
|
||||||
|
@ -284,7 +212,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||||
_sliderPosition.valueTo = dur
|
_sliderPosition.valueTo = dur
|
||||||
|
|
||||||
if (d.canSetVolume) {
|
if (d.canSetVolume()) {
|
||||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||||
_layoutVolumeFixed.visibility = View.GONE;
|
_layoutVolumeFixed.visibility = View.GONE;
|
||||||
} else {
|
} else {
|
||||||
|
@ -306,13 +234,11 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
CastConnectionState.CONNECTED -> {
|
CastConnectionState.CONNECTED -> {
|
||||||
enableControls(interactiveControls)
|
enableControls(interactiveControls)
|
||||||
}
|
}
|
||||||
CastConnectionState.CONNECTING,
|
CastConnectionState.CONNECTING, CastConnectionState.DISCONNECTED -> {
|
||||||
CastConnectionState.DISCONNECTED -> {
|
|
||||||
disableControls(interactiveControls)
|
disableControls(interactiveControls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableControls(views: List<View>) {
|
private fun enableControls(views: List<View>) {
|
||||||
views.forEach { enableControl(it) }
|
views.forEach { enableControl(it) }
|
||||||
|
|
|
@ -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.activities.SettingsActivity
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
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.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.models.PlatformVideoWithTime
|
import com.futo.platformplayer.models.PlatformVideoWithTime
|
||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
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.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
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.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
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.ScriptReloadRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
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.fixHtmlLinks
|
||||||
import com.futo.platformplayer.fixHtmlWhitespace
|
import com.futo.platformplayer.fixHtmlWhitespace
|
||||||
import com.futo.platformplayer.getNowDiffSeconds
|
import com.futo.platformplayer.getNowDiffSeconds
|
||||||
|
@ -177,7 +176,6 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.fcast.sender_sdk.DeviceFeature
|
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
@ -581,7 +579,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
|
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
|
||||||
val ad = StateCasting.instance.activeDevice
|
val ad = StateCasting.instance.activeDevice
|
||||||
if (ad != null) {
|
if (ad != null) {
|
||||||
ad.seekVideo(chapter.timeEnd)
|
ad.seekTo(chapter.timeEnd)
|
||||||
} else {
|
} else {
|
||||||
_player.seekTo((chapter.timeEnd * 1000).toLong());
|
_player.seekTo((chapter.timeEnd * 1000).toLong());
|
||||||
}
|
}
|
||||||
|
@ -667,50 +665,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isInEditMode) {
|
if (!isInEditMode) {
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
|
||||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState ->
|
|
||||||
if (_onPauseCalled) {
|
|
||||||
return@subscribe;
|
|
||||||
}
|
|
||||||
|
|
||||||
when (connectionState) {
|
|
||||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> {
|
|
||||||
loadCurrentVideo(lastPositionMilliseconds);
|
|
||||||
updatePillButtonVisibilities();
|
|
||||||
setCastEnabled(true);
|
|
||||||
}
|
|
||||||
com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> {
|
|
||||||
loadCurrentVideo(lastPositionMilliseconds, playWhenReady = device.isPlaying);
|
|
||||||
updatePillButtonVisibilities();
|
|
||||||
setCastEnabled(false);
|
|
||||||
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpStateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
|
||||||
val activeDevice = StateCasting.instance.activeDevice;
|
|
||||||
if (activeDevice != null) {
|
|
||||||
handlePlayChanged(it);
|
|
||||||
|
|
||||||
val v = video;
|
|
||||||
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
|
|
||||||
nextVideo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ExpStateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) {
|
|
||||||
if (_isCasting) {
|
|
||||||
setLastPositionMilliseconds((it * 1000.0).toLong(), true);
|
|
||||||
_cast.setTime(lastPositionMilliseconds);
|
|
||||||
_timeBar.setPosition(it.toLong());
|
|
||||||
_timeBar.setBufferedPosition(0);
|
|
||||||
_timeBar.setDuration(video?.duration ?: 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState ->
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState ->
|
||||||
if (_onPauseCalled) {
|
if (_onPauseCalled) {
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
|
@ -753,7 +707,6 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_timeBar.setDuration(video?.duration ?: 0);
|
_timeBar.setDuration(video?.duration ?: 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
updatePillButtonVisibilities();
|
updatePillButtonVisibilities();
|
||||||
|
|
||||||
|
@ -934,7 +887,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
if (ad != null) {
|
if (ad != null) {
|
||||||
val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong());
|
val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong());
|
||||||
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
||||||
ad.seekVideo(currentChapter.timeEnd);
|
ad.seekTo(currentChapter.timeEnd);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val currentChapter = _player.getCurrentChapter(_player.position);
|
val currentChapter = _player.getCurrentChapter(_player.position);
|
||||||
|
@ -1218,7 +1171,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_onPauseCalled = true;
|
_onPauseCalled = true;
|
||||||
_taskLoadVideo.cancel();
|
_taskLoadVideo.cancel();
|
||||||
|
|
||||||
if (StateCastingDispatcher.isCasting()) {
|
if (StateCasting.instance.isCasting) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1271,15 +1224,9 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_container_content_description.cleanup();
|
_container_content_description.cleanup();
|
||||||
_container_content_support.cleanup();
|
_container_content_support.cleanup();
|
||||||
StatePlayer.instance.autoplayChanged.remove(this)
|
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.onActiveDevicePlayChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||||
}
|
|
||||||
StateApp.instance.preventPictureInPicture.remove(this);
|
StateApp.instance.preventPictureInPicture.remove(this);
|
||||||
StatePlayer.instance.onQueueChanged.remove(this);
|
StatePlayer.instance.onQueueChanged.remove(this);
|
||||||
StatePlayer.instance.onVideoChanging.remove(this);
|
StatePlayer.instance.onVideoChanging.remove(this);
|
||||||
|
@ -2011,7 +1958,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StateCastingDispatcher.isCasting()) {
|
if (!StateCasting.instance.isCasting) {
|
||||||
setCastEnabled(false);
|
setCastEnabled(false);
|
||||||
|
|
||||||
val isLimitedVersion = StatePlatform.instance.getContentClientOrNull(video.url)?.let {
|
val isLimitedVersion = StatePlatform.instance.getContentClientOrNull(video.url)?.let {
|
||||||
|
@ -2087,19 +2034,11 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
|
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
|
||||||
try {
|
try {
|
||||||
val castingSucceeded = if (Settings.instance.casting.experimentalCasting) {
|
val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
|
||||||
ExpStateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
|
|
||||||
_cast.setLoading(it)
|
_cast.setLoading(it)
|
||||||
}, onLoadingEstimate = {
|
}, onLoadingEstimate = {
|
||||||
_cast.setLoading(it)
|
_cast.setLoading(it)
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
|
|
||||||
_cast.setLoading(it)
|
|
||||||
}, onLoadingEstimate = {
|
|
||||||
_cast.setLoading(it)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (castingSucceeded) {
|
if (castingSucceeded) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
@ -2295,7 +2234,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentPlaybackRate = (if (_isCasting) {
|
val currentPlaybackRate = (if (_isCasting) {
|
||||||
StateCastingDispatcher.getActiveDeviceSpeed()
|
StateCasting.instance.activeDevice?.speed
|
||||||
} else _player.getPlaybackRate()) ?: 1.0
|
} else _player.getPlaybackRate()) ?: 1.0
|
||||||
_overlay_quality_selector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }?.let {
|
_overlay_quality_selector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }?.let {
|
||||||
(it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString())
|
(it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString())
|
||||||
|
@ -2414,9 +2353,9 @@ class VideoDetailView : ConstraintLayout {
|
||||||
?.distinct()
|
?.distinct()
|
||||||
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
||||||
|
|
||||||
val canSetSpeed = !_isCasting || StateCastingDispatcher.canActiveDeviceSetSpeed();
|
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed() ?: false
|
||||||
val currentPlaybackRate = if (_isCasting) {
|
val currentPlaybackRate = if (_isCasting) {
|
||||||
StateCastingDispatcher.getActiveDeviceSpeed()
|
StateCasting.instance.activeDevice?.speed
|
||||||
} else {
|
} else {
|
||||||
_player.getPlaybackRate()
|
_player.getPlaybackRate()
|
||||||
}
|
}
|
||||||
|
@ -2434,7 +2373,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
setButtons(playbackLabels, String.format(Locale.US, format, currentPlaybackRate));
|
setButtons(playbackLabels, String.format(Locale.US, format, currentPlaybackRate));
|
||||||
onClick.subscribe { v ->
|
onClick.subscribe { v ->
|
||||||
val currentPlaybackSpeed = if (_isCasting) {
|
val currentPlaybackSpeed = if (_isCasting) {
|
||||||
StateCastingDispatcher.getActiveDeviceSpeed()
|
StateCasting.instance.activeDevice?.speed
|
||||||
} else _player.getPlaybackRate();
|
} else _player.getPlaybackRate();
|
||||||
var playbackSpeedString = v;
|
var playbackSpeedString = v;
|
||||||
val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep();
|
val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep();
|
||||||
|
@ -2443,9 +2382,11 @@ class VideoDetailView : ConstraintLayout {
|
||||||
else if(v == "-")
|
else if(v == "-")
|
||||||
playbackSpeedString = String.format(Locale.US, "%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString();
|
playbackSpeedString = String.format(Locale.US, "%.2f", Math.max(0.1, (currentPlaybackSpeed?.toDouble() ?: 1.0) - stepSpeed)).toString();
|
||||||
val newPlaybackSpeed = playbackSpeedString.toDouble();
|
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)})");
|
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);
|
setSelected(playbackSpeedString);
|
||||||
} else {
|
} else {
|
||||||
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
||||||
|
@ -2561,7 +2502,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
//Handlers
|
//Handlers
|
||||||
private fun handlePlay() {
|
private fun handlePlay() {
|
||||||
Logger.i(TAG, "handlePlay")
|
Logger.i(TAG, "handlePlay")
|
||||||
if (!StateCastingDispatcher.resumeVideo()) {
|
if (!StateCasting.instance.resumeVideo()) {
|
||||||
_player.play()
|
_player.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2577,19 +2518,19 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
private fun handlePause() {
|
private fun handlePause() {
|
||||||
Logger.i(TAG, "handlePause")
|
Logger.i(TAG, "handlePause")
|
||||||
if (!StateCastingDispatcher.pauseVideo()) {
|
if (!StateCasting.instance.pauseVideo()) {
|
||||||
_player.pause()
|
_player.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun handleSeek(ms: Long) {
|
private fun handleSeek(ms: Long) {
|
||||||
Logger.i(TAG, "handleSeek(ms=$ms)")
|
Logger.i(TAG, "handleSeek(ms=$ms)")
|
||||||
if (!StateCastingDispatcher.videoSeekTo(ms.toDouble() / 1000.0)) {
|
if (!StateCasting.instance.videoSeekTo(ms.toDouble() / 1000.0)) {
|
||||||
_player.seekTo(ms)
|
_player.seekTo(ms)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun handleStop() {
|
private fun handleStop() {
|
||||||
Logger.i(TAG, "handleStop")
|
Logger.i(TAG, "handleStop")
|
||||||
if (!StateCastingDispatcher.stopVideo()) {
|
if (!StateCasting.instance.stopVideo()) {
|
||||||
_player.stop()
|
_player.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2597,7 +2538,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
private fun handlePlayChanged(playing: Boolean) {
|
private fun handlePlayChanged(playing: Boolean) {
|
||||||
Logger.i(TAG, "handlePlayChanged(playing=$playing)")
|
Logger.i(TAG, "handlePlayChanged(playing=$playing)")
|
||||||
|
|
||||||
if (StateCastingDispatcher.isCasting()) {
|
if (StateCasting.instance.isCasting) {
|
||||||
_cast.setIsPlaying(playing);
|
_cast.setIsPlaying(playing);
|
||||||
} else {
|
} else {
|
||||||
StatePlayer.instance.updateMediaSession( null);
|
StatePlayer.instance.updateMediaSession( null);
|
||||||
|
@ -2639,9 +2580,9 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (StateCastingDispatcher.isConnected()) {
|
if (StateCasting.instance.activeDevice != null) {
|
||||||
val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: 0.0
|
val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0
|
||||||
val speed = StateCastingDispatcher.getActiveDeviceSpeed() ?: 1.0
|
val speed = StateCasting.instance.activeDevice?.speed ?: 1.0
|
||||||
castIfAvailable(
|
castIfAvailable(
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
video,
|
video,
|
||||||
|
@ -2670,9 +2611,9 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (StateCastingDispatcher.isConnected()) {
|
if (StateCasting.instance.activeDevice != null) {
|
||||||
val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: 0.0
|
val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0
|
||||||
val speed = StateCastingDispatcher.getActiveDeviceSpeed() ?: 1.0
|
val speed = StateCasting.instance.activeDevice?.speed ?: 1.0
|
||||||
castIfAvailable(
|
castIfAvailable(
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
video,
|
video,
|
||||||
|
@ -2702,9 +2643,9 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
if (StateCastingDispatcher.isConnected()) {
|
if (StateCasting.instance.activeDevice != null) {
|
||||||
val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: 0.0
|
val expectedCurrentTime = StateCasting.instance.activeDevice?.expectedCurrentTime ?: 0.0
|
||||||
val speed = StateCastingDispatcher.getActiveDeviceSpeed() ?: 1.0
|
val speed = StateCasting.instance.activeDevice?.speed ?: 1.0
|
||||||
castIfAvailable(
|
castIfAvailable(
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
video,
|
video,
|
||||||
|
|
|
@ -1,34 +1,11 @@
|
||||||
package com.futo.platformplayer.models
|
package com.futo.platformplayer.models
|
||||||
|
|
||||||
import com.futo.platformplayer.casting.CastProtocolType
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
import com.futo.platformplayer.experimental_casting.ExpCastProtocolType
|
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class CastingDeviceInfo {
|
class CastingDeviceInfo(
|
||||||
var name: String;
|
var name: String,
|
||||||
var type: CastProtocolType;
|
var type: CastProtocolType,
|
||||||
var addresses: Array<String>;
|
var addresses: Array<String>,
|
||||||
var port: Int;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.DevJSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.background.BackgroundWorker
|
import com.futo.platformplayer.background.BackgroundWorker
|
||||||
|
import com.futo.platformplayer.casting.OldStateCasting
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
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.HomeFragment
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||||
import com.futo.platformplayer.logging.AndroidLogConsumer
|
import com.futo.platformplayer.logging.AndroidLogConsumer
|
||||||
|
@ -760,11 +760,7 @@ class StateApp {
|
||||||
_connectivityManager?.unregisterNetworkCallback(_connectivityEvents);
|
_connectivityManager?.unregisterNetworkCallback(_connectivityEvents);
|
||||||
|
|
||||||
StatePlayer.instance.closeMediaSession();
|
StatePlayer.instance.closeMediaSession();
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
|
||||||
ExpStateCasting.instance.stop()
|
|
||||||
} else {
|
|
||||||
StateCasting.instance.stop()
|
StateCasting.instance.stop()
|
||||||
}
|
|
||||||
StateSync.instance.stop();
|
StateSync.instance.stop();
|
||||||
StatePlayer.dispose();
|
StatePlayer.dispose();
|
||||||
Companion.dispose();
|
Companion.dispose();
|
||||||
|
|
|
@ -6,34 +6,14 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
import com.futo.platformplayer.casting.CastingDevice
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.experimental_casting.CastingDeviceHandle
|
|
||||||
|
|
||||||
sealed class GenericCastingDevice {
|
data class DeviceAdapterEntry(val castingDevice: CastingDevice, val isPinnedDevice: Boolean, val isOnlineDevice: Boolean)
|
||||||
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)
|
|
||||||
|
|
||||||
class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
|
class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
|
||||||
private val _devices: List<DeviceAdapterEntry>;
|
private val _devices: List<DeviceAdapterEntry>;
|
||||||
|
|
||||||
var onPin = Event1<GenericCastingDevice>();
|
var onPin = Event1<CastingDevice>();
|
||||||
var onConnect = Event1<GenericCastingDevice>();
|
var onConnect = Event1<CastingDevice>();
|
||||||
|
|
||||||
constructor(devices: List<DeviceAdapterEntry>) : super() {
|
constructor(devices: List<DeviceAdapterEntry>) : super() {
|
||||||
_devices = devices;
|
_devices = devices;
|
||||||
|
|
|
@ -9,15 +9,13 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
import com.futo.platformplayer.casting.FCastCastingDevice
|
import com.futo.platformplayer.casting.CastingDevice
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
|
||||||
import org.fcast.sender_sdk.ProtocolType
|
|
||||||
|
|
||||||
class DeviceViewHolder : ViewHolder {
|
class DeviceViewHolder : ViewHolder {
|
||||||
private val _layoutDevice: FrameLayout;
|
private val _layoutDevice: FrameLayout;
|
||||||
|
@ -31,11 +29,11 @@ class DeviceViewHolder : ViewHolder {
|
||||||
private var _animatableLoader: Animatable? = null;
|
private var _animatableLoader: Animatable? = null;
|
||||||
private var _imagePin: ImageView;
|
private var _imagePin: ImageView;
|
||||||
|
|
||||||
var device: GenericCastingDevice? = null
|
var device: CastingDevice? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var onPin = Event1<GenericCastingDevice>();
|
var onPin = Event1<CastingDevice>();
|
||||||
val onConnect = Event1<GenericCastingDevice>();
|
val onConnect = Event1<CastingDevice>();
|
||||||
|
|
||||||
constructor(view: View) : super(view) {
|
constructor(view: View) : super(view) {
|
||||||
_root = view.findViewById(R.id.layout_root);
|
_root = view.findViewById(R.id.layout_root);
|
||||||
|
@ -55,41 +53,17 @@ class DeviceViewHolder : ViewHolder {
|
||||||
|
|
||||||
val connect = {
|
val connect = {
|
||||||
device?.let { dev ->
|
device?.let { dev ->
|
||||||
when (dev) {
|
try {
|
||||||
is GenericCastingDevice.Normal -> {
|
if (dev.isReady) {
|
||||||
if (dev.device.isReady) {
|
StateCasting.instance.activeDevice?.stopPlayback()
|
||||||
// NOTE: we assume normal casting is used
|
StateCasting.instance.connectDevice(dev)
|
||||||
StateCasting.instance.activeDevice?.stopCasting()
|
|
||||||
StateCasting.instance.connectDevice(dev.device)
|
|
||||||
onConnect.emit(dev)
|
onConnect.emit(dev)
|
||||||
} else {
|
} else {
|
||||||
try {
|
view.context?.let {
|
||||||
view.context?.let { UIDialogs.toast(it, "Device not ready, may be offline") }
|
UIDialogs.toast(it, "Device not ready, may be offline")
|
||||||
} catch (e: Throwable) {
|
|
||||||
//Ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is GenericCastingDevice.Experimental -> {
|
|
||||||
if (dev.handle.device.isReady()) {
|
|
||||||
// NOTE: we assume experimental casting is used
|
|
||||||
try {
|
|
||||||
ExpStateCasting.instance.activeDevice?.device?.stopPlayback()
|
|
||||||
ExpStateCasting.instance.activeDevice?.device?.disconnect()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
//Ignored
|
|
||||||
}
|
|
||||||
ExpStateCasting.instance.connectDevice(dev.handle)
|
|
||||||
onConnect.emit(dev)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
view.context?.let { UIDialogs.toast(it, "Device not ready, may be offline") }
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
//Ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (_: Throwable) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,26 +77,31 @@ class DeviceViewHolder : ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
||||||
|
when (d.protocolType) {
|
||||||
fun bind(d: GenericCastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
CastProtocolType.CHROMECAST -> {
|
||||||
when (d) {
|
|
||||||
is GenericCastingDevice.Normal -> {
|
|
||||||
if (d.device is ChromecastCastingDevice) {
|
|
||||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||||
_textType.text = "Chromecast";
|
_textType.text = "Chromecast";
|
||||||
} else if (d.device is AirPlayCastingDevice) {
|
}
|
||||||
|
CastProtocolType.AIRPLAY -> {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||||
_textType.text = "AirPlay";
|
_textType.text = "AirPlay";
|
||||||
} else if (d.device is FCastCastingDevice) {
|
}
|
||||||
|
CastProtocolType.FCAST -> {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
_imageDevice.setImageResource(R.drawable.ic_exp_fc)
|
||||||
|
} else {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
_imageDevice.setImageResource(R.drawable.ic_fc);
|
||||||
|
}
|
||||||
_textType.text = "FCast";
|
_textType.text = "FCast";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_textName.text = d.device.name;
|
_textName.text = d.name;
|
||||||
_imageOnline.visibility = if (isOnlineDevice && d.device.isReady) View.VISIBLE else View.GONE
|
_imageOnline.visibility = if (isOnlineDevice && d.isReady) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
if (!d.device.isReady) {
|
|
||||||
|
if (!d.isReady) {
|
||||||
_imageLoader.visibility = View.GONE;
|
_imageLoader.visibility = View.GONE;
|
||||||
_textNotReady.visibility = View.VISIBLE;
|
_textNotReady.visibility = View.VISIBLE;
|
||||||
_imagePin.visibility = View.GONE;
|
_imagePin.visibility = View.GONE;
|
||||||
|
@ -130,7 +109,7 @@ class DeviceViewHolder : ViewHolder {
|
||||||
_textNotReady.visibility = View.GONE;
|
_textNotReady.visibility = View.GONE;
|
||||||
|
|
||||||
val dev = StateCasting.instance.activeDevice;
|
val dev = StateCasting.instance.activeDevice;
|
||||||
if (dev == d.device) {
|
if (dev == d) {
|
||||||
if (dev.connectionState == CastConnectionState.CONNECTED) {
|
if (dev.connectionState == CastConnectionState.CONNECTED) {
|
||||||
_imageLoader.visibility = View.GONE;
|
_imageLoader.visibility = View.GONE;
|
||||||
_textNotReady.visibility = View.GONE;
|
_textNotReady.visibility = View.GONE;
|
||||||
|
@ -141,7 +120,7 @@ class DeviceViewHolder : ViewHolder {
|
||||||
_imagePin.visibility = View.VISIBLE;
|
_imagePin.visibility = View.VISIBLE;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (d.device.isReady) {
|
if (d.isReady) {
|
||||||
_imageLoader.visibility = View.GONE;
|
_imageLoader.visibility = View.GONE;
|
||||||
_textNotReady.visibility = View.GONE;
|
_textNotReady.visibility = View.GONE;
|
||||||
_imagePin.visibility = View.VISIBLE;
|
_imagePin.visibility = View.VISIBLE;
|
||||||
|
@ -163,62 +142,4 @@ class DeviceViewHolder : ViewHolder {
|
||||||
|
|
||||||
device = d;
|
device = d;
|
||||||
}
|
}
|
||||||
is GenericCastingDevice.Experimental -> {
|
|
||||||
when (d.handle.device.castingProtocol()) {
|
|
||||||
ProtocolType.CHROMECAST -> {
|
|
||||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
|
||||||
_textType.text = "Chromecast";
|
|
||||||
}
|
|
||||||
ProtocolType.F_CAST -> {
|
|
||||||
_imageDevice.setImageResource(R.drawable.ic_exp_fc);
|
|
||||||
_textType.text = "FCast";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_textName.text = d.handle.device.name();
|
|
||||||
_imageOnline.visibility = if (isOnlineDevice && d.handle.device.isReady()) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
if (!d.handle.device.isReady()) {
|
|
||||||
_imageLoader.visibility = View.GONE;
|
|
||||||
_textNotReady.visibility = View.VISIBLE;
|
|
||||||
_imagePin.visibility = View.GONE;
|
|
||||||
} else {
|
|
||||||
_textNotReady.visibility = View.GONE;
|
|
||||||
|
|
||||||
val dev = ExpStateCasting.instance.activeDevice;
|
|
||||||
if (dev == d.handle) {
|
|
||||||
if (dev.connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED) {
|
|
||||||
_imageLoader.visibility = View.GONE;
|
|
||||||
_textNotReady.visibility = View.GONE;
|
|
||||||
_imagePin.visibility = View.VISIBLE;
|
|
||||||
} else {
|
|
||||||
_imageLoader.visibility = View.VISIBLE;
|
|
||||||
_textNotReady.visibility = View.GONE;
|
|
||||||
_imagePin.visibility = View.VISIBLE;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (d.handle.device.isReady()) {
|
|
||||||
_imageLoader.visibility = View.GONE;
|
|
||||||
_textNotReady.visibility = View.GONE;
|
|
||||||
_imagePin.visibility = View.VISIBLE;
|
|
||||||
} else {
|
|
||||||
_imageLoader.visibility = View.GONE;
|
|
||||||
_textNotReady.visibility = View.VISIBLE;
|
|
||||||
_imagePin.visibility = View.VISIBLE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_imagePin.setImageResource(if (isPinnedDevice) R.drawable.keep_24px else R.drawable.ic_pin)
|
|
||||||
|
|
||||||
if (_imageLoader.isVisible) {
|
|
||||||
_animatableLoader?.start();
|
|
||||||
} else {
|
|
||||||
_animatableLoader?.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
device = d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -2,21 +2,16 @@ package com.futo.platformplayer.views.casting
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
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 androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
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.CastConnectionState.*
|
||||||
|
import com.futo.platformplayer.casting.OldStateCasting
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
|
||||||
|
|
||||||
class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
||||||
var onClick = Event1<Pair<String, Any>>();
|
var onClick = Event1<Pair<String, Any>>();
|
||||||
|
@ -29,14 +24,8 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
||||||
visibility = View.GONE;
|
visibility = View.GONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
|
||||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
|
|
||||||
updateCastState();
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
|
||||||
updateCastState();
|
updateCastState()
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCastState();
|
updateCastState();
|
||||||
|
@ -45,23 +34,7 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
||||||
|
|
||||||
private fun updateCastState() {
|
private fun updateCastState() {
|
||||||
val c = context ?: return;
|
val c = context ?: return;
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
|
||||||
val d = ExpStateCasting.instance.activeDevice;
|
|
||||||
|
|
||||||
val activeColor = ContextCompat.getColor(c, R.color.colorPrimary);
|
|
||||||
val connectingColor = ContextCompat.getColor(c, R.color.gray_c3);
|
|
||||||
val inactiveColor = ContextCompat.getColor(c, R.color.white);
|
|
||||||
|
|
||||||
if (d != null) {
|
|
||||||
when (d.connectionState) {
|
|
||||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> setColorFilter(activeColor)
|
|
||||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTING -> setColorFilter(connectingColor)
|
|
||||||
com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> setColorFilter(activeColor)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setColorFilter(inactiveColor);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
|
|
||||||
val activeColor = ContextCompat.getColor(c, R.color.colorPrimary);
|
val activeColor = ContextCompat.getColor(c, R.color.colorPrimary);
|
||||||
|
@ -70,22 +43,17 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
||||||
|
|
||||||
if (d != null) {
|
if (d != null) {
|
||||||
when (d.connectionState) {
|
when (d.connectionState) {
|
||||||
CastConnectionState.CONNECTED -> setColorFilter(activeColor)
|
DISCONNECTED -> setColorFilter(activeColor)
|
||||||
CastConnectionState.CONNECTING -> setColorFilter(connectingColor)
|
CONNECTING -> setColorFilter(connectingColor)
|
||||||
CastConnectionState.DISCONNECTED -> setColorFilter(activeColor)
|
CONNECTED -> setColorFilter(activeColor)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setColorFilter(inactiveColor);
|
setColorFilter(inactiveColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
setOnClickListener(null);
|
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.Settings
|
||||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
|
||||||
import com.futo.platformplayer.experimental_casting.StateCastingDispatcher
|
|
||||||
import com.futo.platformplayer.formatDuration
|
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.states.StatePlayer
|
||||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
import com.futo.platformplayer.views.TargetTapLoaderView
|
||||||
import com.futo.platformplayer.views.behavior.GestureControlView
|
import com.futo.platformplayer.views.behavior.GestureControlView
|
||||||
|
@ -39,9 +34,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.fcast.sender_sdk.DeviceFeature
|
|
||||||
|
|
||||||
class CastView : ConstraintLayout {
|
class CastView : ConstraintLayout {
|
||||||
private val _thumbnail: ImageView;
|
private val _thumbnail: ImageView;
|
||||||
|
@ -100,51 +93,40 @@ class CastView : ConstraintLayout {
|
||||||
_gestureControlView.fullScreenGestureEnabled = false
|
_gestureControlView.fullScreenGestureEnabled = false
|
||||||
_gestureControlView.setupTouchArea();
|
_gestureControlView.setupTouchArea();
|
||||||
_gestureControlView.onSpeedHoldStart.subscribe {
|
_gestureControlView.onSpeedHoldStart.subscribe {
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
val d = StateCasting.instance.activeDevice ?: return@subscribe
|
||||||
val d = ExpStateCasting.instance.activeDevice ?: return@subscribe;
|
|
||||||
_speedHoldWasPlaying = d.isPlaying
|
_speedHoldWasPlaying = d.isPlaying
|
||||||
_speedHoldPrevRate = d.speed
|
_speedHoldPrevRate = d.speed
|
||||||
if (d.device.supportsFeature(DeviceFeature.SET_SPEED)) {
|
if (d.canSetSpeed()) {
|
||||||
try {
|
try {
|
||||||
d.device.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
d.device.resumePlayback()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Ignored
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
|
||||||
_speedHoldWasPlaying = d.isPlaying
|
|
||||||
_speedHoldPrevRate = d.speed
|
|
||||||
if (d.canSetSpeed) {
|
|
||||||
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Ignored
|
||||||
}
|
}
|
||||||
d.resumeVideo()
|
}
|
||||||
|
try {
|
||||||
|
d.resumePlayback()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_gestureControlView.onSpeedHoldEnd.subscribe {
|
_gestureControlView.onSpeedHoldEnd.subscribe {
|
||||||
if (Settings.instance.casting.experimentalCasting) {
|
try {
|
||||||
val d = ExpStateCasting.instance.activeDevice ?: return@subscribe;
|
|
||||||
if (!_speedHoldWasPlaying) {
|
|
||||||
d.device.resumePlayback()
|
|
||||||
}
|
|
||||||
d.device.changeSpeed(_speedHoldPrevRate)
|
|
||||||
} else {
|
|
||||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||||
if (!_speedHoldWasPlaying) {
|
if (!_speedHoldWasPlaying) {
|
||||||
d.pauseVideo()
|
d.resumePlayback()
|
||||||
}
|
}
|
||||||
d.changeSpeed(_speedHoldPrevRate)
|
d.changeSpeed(_speedHoldPrevRate)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_gestureControlView.onSeek.subscribe {
|
_gestureControlView.onSeek.subscribe {
|
||||||
val expectedCurrentTime = StateCastingDispatcher.getExpectedCurrentTime() ?: return@subscribe
|
try {
|
||||||
StateCastingDispatcher.videoSeekTo(expectedCurrentTime + it / 1000)
|
val d = StateCasting.instance.activeDevice ?: return@subscribe
|
||||||
|
val expectedCurrentTime = d.expectedCurrentTime
|
||||||
|
d.seekTo(expectedCurrentTime + it / 1000)
|
||||||
|
} catch (_: Throwable) { }
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonLoop.setOnClickListener {
|
_buttonLoop.setOnClickListener {
|
||||||
|
@ -155,25 +137,35 @@ class CastView : ConstraintLayout {
|
||||||
|
|
||||||
_timeBar.addListener(object : TimeBar.OnScrubListener {
|
_timeBar.addListener(object : TimeBar.OnScrubListener {
|
||||||
override fun onScrubStart(timeBar: TimeBar, position: Long) {
|
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) {
|
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) {
|
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(); };
|
_buttonMinimize.setOnClickListener { onMinimizeClick.emit(); };
|
||||||
_buttonSettings.setOnClickListener { onSettingsClick.emit(); };
|
_buttonSettings.setOnClickListener { onSettingsClick.emit(); };
|
||||||
_buttonPlay.setOnClickListener {
|
_buttonPlay.setOnClickListener {
|
||||||
StateCastingDispatcher.resumeVideo()
|
try {
|
||||||
|
StateCasting.instance.activeDevice?.resumePlayback()
|
||||||
|
} catch (_: Throwable) { }
|
||||||
}
|
}
|
||||||
_buttonPause.setOnClickListener {
|
_buttonPause.setOnClickListener {
|
||||||
StateCastingDispatcher.pauseVideo()
|
try {
|
||||||
|
StateCasting.instance.activeDevice?.pausePlayback()
|
||||||
|
} catch (_: Throwable) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isInEditMode) {
|
if (!isInEditMode) {
|
||||||
|
@ -257,25 +249,9 @@ class CastView : ConstraintLayout {
|
||||||
stopTimeJob()
|
stopTimeJob()
|
||||||
|
|
||||||
if(isPlaying) {
|
if(isPlaying) {
|
||||||
// NOTE: the experimental implementation polls automatically
|
StateCasting.instance.startUpdateTimeJob(
|
||||||
if (!Settings.instance.casting.experimentalCasting) {
|
onTimeJobTimeChanged_s
|
||||||
val d = StateCasting.instance.activeDevice;
|
) { setTime(it) }
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_inPictureInPicture) {
|
if (!_inPictureInPicture) {
|
||||||
_buttonPause.visibility = View.VISIBLE;
|
_buttonPause.visibility = View.VISIBLE;
|
||||||
|
@ -287,7 +263,7 @@ class CastView : ConstraintLayout {
|
||||||
_buttonPlay.visibility = View.VISIBLE;
|
_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()) {
|
if(StatePlayer.instance.hasMediaSession()) {
|
||||||
StatePlayer.instance.updateMediaSession(null);
|
StatePlayer.instance.updateMediaSession(null);
|
||||||
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), (position ?: 0));
|
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), (position ?: 0));
|
||||||
|
@ -351,10 +327,10 @@ class CastView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPlaybackStateCompat(): Int {
|
private fun getPlaybackStateCompat(): Int {
|
||||||
if (!StateCastingDispatcher.isConnected()) {
|
if (StateCasting.instance.activeDevice?.connectionState != CastConnectionState.CONNECTED) {
|
||||||
return PlaybackState.STATE_NONE
|
return PlaybackState.STATE_NONE
|
||||||
}
|
}
|
||||||
return when(StateCastingDispatcher.isPlaying()) {
|
return when(StateCasting.instance.activeDevice?.isPlaying) {
|
||||||
true -> PlaybackStateCompat.STATE_PLAYING;
|
true -> PlaybackStateCompat.STATE_PLAYING;
|
||||||
else -> PlaybackStateCompat.STATE_PAUSED;
|
else -> PlaybackStateCompat.STATE_PAUSED;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue