mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-09-12 12:32:27 +00:00
casting: add experimental support for the FCast sender SDK
This commit is contained in:
parent
8c1a18d8b4
commit
1176cee40b
16 changed files with 3226 additions and 356 deletions
|
@ -231,4 +231,7 @@ dependencies {
|
||||||
testImplementation "org.mockito:mockito-core:5.4.0"
|
testImplementation "org.mockito:mockito-core:5.4.0"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
|
||||||
|
//Rust casting SDK
|
||||||
|
implementation "net.java.dev.jna:jna:5.12.0@aar"
|
||||||
}
|
}
|
||||||
|
|
|
@ -719,6 +719,11 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var allowLinkLocalIpv4: Boolean = false;
|
var allowLinkLocalIpv4: Boolean = false;
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
|
||||||
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
|
var experimentalCasting: Boolean = false
|
||||||
|
|
||||||
/*TODO: Should we have a different casting quality?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
|
|
|
@ -38,6 +38,7 @@ 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
|
||||||
|
@ -437,6 +438,32 @@ class UIDialogs {
|
||||||
|
|
||||||
|
|
||||||
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
|
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
val d = ExpStateCasting.instance.activeDevice;
|
||||||
|
if (d != null) {
|
||||||
|
val dialog = ConnectedCastingDialog(context);
|
||||||
|
if (context is Activity) {
|
||||||
|
dialog.setOwnerActivity(context)
|
||||||
|
}
|
||||||
|
registerDialogOpened(dialog);
|
||||||
|
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||||
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
|
dialog.show();
|
||||||
|
} else {
|
||||||
|
val dialog = ConnectCastingDialog(context);
|
||||||
|
if (context is Activity) {
|
||||||
|
dialog.setOwnerActivity(context)
|
||||||
|
}
|
||||||
|
registerDialogOpened(dialog);
|
||||||
|
val c = context
|
||||||
|
if (c is Activity) {
|
||||||
|
dialog.setOwnerActivity(c);
|
||||||
|
}
|
||||||
|
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||||
|
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
if (d != null) {
|
if (d != null) {
|
||||||
val dialog = ConnectedCastingDialog(context);
|
val dialog = ConnectedCastingDialog(context);
|
||||||
|
@ -462,6 +489,7 @@ class UIDialogs {
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
|
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
|
||||||
val dialog = CastingHelpDialog(context);
|
val dialog = CastingHelpDialog(context);
|
||||||
|
|
|
@ -42,6 +42,7 @@ 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
|
||||||
|
@ -507,7 +508,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
handleIntent(intent);
|
handleIntent(intent);
|
||||||
|
|
||||||
if (Settings.instance.casting.enabled) {
|
if (Settings.instance.casting.enabled) {
|
||||||
StateCasting.instance.start(this);
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.start(this)
|
||||||
|
} else {
|
||||||
|
StateCasting.instance.start(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatePlatform.instance.onDevSourceChanged.subscribe {
|
StatePlatform.instance.onDevSourceChanged.subscribe {
|
||||||
|
@ -1046,7 +1051,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
Logger.i(TAG, "handleFCast");
|
Logger.i(TAG, "handleFCast");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.handleUrl(this, url)
|
||||||
|
} else {
|
||||||
StateCasting.instance.handleUrl(this, url)
|
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)
|
||||||
|
|
|
@ -8,9 +8,11 @@ import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
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.CastProtocolType
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
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
|
||||||
|
|
||||||
|
@ -101,7 +103,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());
|
||||||
StateCasting.instance.addRememberedDevice(castingDeviceInfo);
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.addRememberedDevice(castingDeviceInfo)
|
||||||
|
} else {
|
||||||
|
StateCasting.instance.addRememberedDevice(castingDeviceInfo)
|
||||||
|
}
|
||||||
performDismiss();
|
performDismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,18 @@ 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.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
|
||||||
|
|
||||||
|
@ -55,17 +58,33 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
_recyclerDevices.layoutManager = LinearLayoutManager(context);
|
_recyclerDevices.layoutManager = LinearLayoutManager(context);
|
||||||
|
|
||||||
_adapter.onPin.subscribe { d ->
|
_adapter.onPin.subscribe { d ->
|
||||||
val isRemembered = _rememberedDevices.contains(d.name)
|
when (d) {
|
||||||
|
is GenericCastingDevice.Experimental -> {
|
||||||
|
val isRemembered = _rememberedDevices.contains(d.handle.device.name())
|
||||||
val newIsRemembered = !isRemembered
|
val newIsRemembered = !isRemembered
|
||||||
if (newIsRemembered) {
|
if (newIsRemembered) {
|
||||||
StateCasting.instance.addRememberedDevice(d)
|
ExpStateCasting.instance.addRememberedDevice(d.handle)
|
||||||
val name = d.name
|
val name = d.handle.device.name()
|
||||||
|
_rememberedDevices.add(name)
|
||||||
|
} else {
|
||||||
|
ExpStateCasting.instance.removeRememberedDevice(d.handle)
|
||||||
|
_rememberedDevices.remove(d.handle.device.name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is GenericCastingDevice.Normal -> {
|
||||||
|
val isRemembered = _rememberedDevices.contains(d.device.name)
|
||||||
|
val newIsRemembered = !isRemembered
|
||||||
|
if (newIsRemembered) {
|
||||||
|
StateCasting.instance.addRememberedDevice(d.device)
|
||||||
|
val name = d.device.name
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
_rememberedDevices.add(name)
|
_rememberedDevices.add(name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StateCasting.instance.removeRememberedDevice(d)
|
StateCasting.instance.removeRememberedDevice(d.device)
|
||||||
_rememberedDevices.remove(d.name)
|
_rememberedDevices.remove(d.device.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
}
|
}
|
||||||
|
@ -105,13 +124,48 @@ 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(StateCasting.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.mapNotNull { it.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
_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)
|
||||||
|
@ -120,9 +174,13 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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(d, _unifiedDevices[index].isPinnedDevice, _unifiedDevices[index].isOnlineDevice)
|
_unifiedDevices[index] = DeviceAdapterEntry(
|
||||||
|
GenericCastingDevice.Normal(d),
|
||||||
|
_unifiedDevices[index].isPinnedDevice,
|
||||||
|
_unifiedDevices[index].isOnlineDevice
|
||||||
|
)
|
||||||
_adapter.notifyItemChanged(index)
|
_adapter.notifyItemChanged(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,6 +198,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun dismiss() {
|
override fun dismiss() {
|
||||||
super.dismiss()
|
super.dismiss()
|
||||||
|
@ -160,16 +219,16 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -184,24 +243,67 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildUnifiedList(): List<DeviceAdapterEntry> {
|
private fun buildUnifiedList(): List<DeviceAdapterEntry> {
|
||||||
val onlineDevices = StateCasting.instance.devices.values.associateBy { it.name }
|
|
||||||
val rememberedDevices = StateCasting.instance.getRememberedCastingDevices().associateBy { it.name }
|
|
||||||
|
|
||||||
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)
|
val intersectionNames = _devices.intersect(_rememberedDevices)
|
||||||
for (name in intersectionNames) {
|
for (name in intersectionNames) {
|
||||||
onlineDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, true, true)) }
|
onlineDevices[name]?.let {
|
||||||
|
unifiedList.add(DeviceAdapterEntry(
|
||||||
|
GenericCastingDevice.Experimental(it), true, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val onlineOnlyNames = _devices - _rememberedDevices
|
val onlineOnlyNames = _devices - _rememberedDevices
|
||||||
for (name in onlineOnlyNames) {
|
for (name in onlineOnlyNames) {
|
||||||
onlineDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, false, true)) }
|
onlineDevices[name]?.let {
|
||||||
|
unifiedList.add(DeviceAdapterEntry(
|
||||||
|
GenericCastingDevice.Experimental(it), false, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val rememberedOnlyNames = _rememberedDevices - _devices
|
val rememberedOnlyNames = _rememberedDevices - _devices
|
||||||
for (name in rememberedOnlyNames) {
|
for (name in rememberedOnlyNames) {
|
||||||
rememberedDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, true, false)) }
|
rememberedDevices[name]?.let {
|
||||||
|
unifiedList.add(DeviceAdapterEntry(
|
||||||
|
GenericCastingDevice.Experimental(it), true, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val onlineDevices = StateCasting.instance.devices.values.associateBy { it.name }
|
||||||
|
val rememberedDevices = StateCasting.instance.getRememberedCastingDevices().associateBy { it.name }
|
||||||
|
|
||||||
|
val intersectionNames = _devices.intersect(_rememberedDevices)
|
||||||
|
for (name in intersectionNames) {
|
||||||
|
onlineDevices[name]?.let {
|
||||||
|
unifiedList.add(DeviceAdapterEntry(
|
||||||
|
GenericCastingDevice.Normal(it), true, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onlineOnlyNames = _devices - _rememberedDevices
|
||||||
|
for (name in onlineOnlyNames) {
|
||||||
|
onlineDevices[name]?.let {
|
||||||
|
unifiedList.add(DeviceAdapterEntry(
|
||||||
|
GenericCastingDevice.Normal( it), false, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val rememberedOnlyNames = _rememberedDevices - _devices
|
||||||
|
for (name in rememberedOnlyNames) {
|
||||||
|
rememberedDevices[name]?.let {
|
||||||
|
unifiedList.add(DeviceAdapterEntry(
|
||||||
|
GenericCastingDevice.Normal(it), true, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return unifiedList
|
return unifiedList
|
||||||
|
|
|
@ -12,6 +12,7 @@ import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
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
|
||||||
|
@ -19,6 +20,7 @@ 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.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
|
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||||
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
|
||||||
|
@ -26,6 +28,8 @@ 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
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.fcast.sender_sdk.DeviceFeature
|
||||||
|
import org.fcast.sender_sdk.ProtocolType
|
||||||
|
|
||||||
class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
private lateinit var _buttonClose: Button;
|
private lateinit var _buttonClose: Button;
|
||||||
|
@ -69,19 +73,31 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
|
|
||||||
_buttonPlay = findViewById(R.id.button_play);
|
_buttonPlay = findViewById(R.id.button_play);
|
||||||
_buttonPlay.setOnClickListener {
|
_buttonPlay.setOnClickListener {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.activeDevice?.device?.resumePlayback()
|
||||||
|
} else {
|
||||||
StateCasting.instance.activeDevice?.resumeVideo()
|
StateCasting.instance.activeDevice?.resumeVideo()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_buttonPause = findViewById(R.id.button_pause);
|
_buttonPause = findViewById(R.id.button_pause);
|
||||||
_buttonPause.setOnClickListener {
|
_buttonPause.setOnClickListener {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.activeDevice?.device?.pausePlayback()
|
||||||
|
} else {
|
||||||
StateCasting.instance.activeDevice?.pauseVideo()
|
StateCasting.instance.activeDevice?.pauseVideo()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_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()
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.activeDevice?.device?.stopPlayback()
|
||||||
|
} else {
|
||||||
StateCasting.instance.activeDevice?.stopVideo()
|
StateCasting.instance.activeDevice?.stopVideo()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_buttonNext = findViewById(R.id.button_next);
|
_buttonNext = findViewById(R.id.button_next);
|
||||||
_buttonNext.setOnClickListener {
|
_buttonNext.setOnClickListener {
|
||||||
|
@ -90,7 +106,11 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
|
|
||||||
_buttonClose.setOnClickListener { dismiss(); };
|
_buttonClose.setOnClickListener { dismiss(); };
|
||||||
_buttonDisconnect.setOnClickListener {
|
_buttonDisconnect.setOnClickListener {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.activeDevice?.device?.stopCasting()
|
||||||
|
} else {
|
||||||
StateCasting.instance.activeDevice?.stopCasting();
|
StateCasting.instance.activeDevice?.stopCasting();
|
||||||
|
}
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -99,11 +119,20 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
return@OnChangeListener
|
return@OnChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
val activeDevice = ExpStateCasting.instance.activeDevice ?: return@OnChangeListener;
|
||||||
|
try {
|
||||||
|
activeDevice.device.seek(value.toDouble());
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to seek.", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
|
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
|
||||||
try {
|
try {
|
||||||
activeDevice.seekVideo(value.toDouble());
|
activeDevice.seekVideo(value.toDouble());
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to change volume.", e);
|
Logger.e(TAG, "Failed to seek.", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -113,6 +142,16 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
return@OnChangeListener
|
return@OnChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
val activeDevice = ExpStateCasting.instance.activeDevice ?: return@OnChangeListener;
|
||||||
|
if (activeDevice.device.supportsFeature(DeviceFeature.SET_VOLUME)) {
|
||||||
|
try {
|
||||||
|
activeDevice.device.changeVolume(value.toDouble());
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to change volume.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
|
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
|
||||||
if (activeDevice.canSetVolume) {
|
if (activeDevice.canSetVolume) {
|
||||||
try {
|
try {
|
||||||
|
@ -121,6 +160,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
Logger.e(TAG, "Failed to change volume.", e);
|
Logger.e(TAG, "Failed to change volume.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -131,6 +171,35 @@ 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) {
|
||||||
|
ExpStateCasting.instance.onActiveDeviceVolumeChanged.remove(this)
|
||||||
|
ExpStateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
||||||
|
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpStateCasting.instance.onActiveDeviceTimeChanged.remove(this)
|
||||||
|
StateCasting.instance.onActiveDeviceTimeChanged.subscribe {
|
||||||
|
_sliderPosition.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderPosition.valueTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpStateCasting.instance.onActiveDeviceDurationChanged.remove(this)
|
||||||
|
ExpStateCasting.instance.onActiveDeviceDurationChanged.subscribe {
|
||||||
|
val dur = it.toFloat().coerceAtLeast(1.0f)
|
||||||
|
_sliderPosition.value = _sliderPosition.value.coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||||
|
_sliderPosition.valueTo = dur
|
||||||
|
}
|
||||||
|
|
||||||
|
_device = StateCasting.instance.activeDevice
|
||||||
|
val d = _device
|
||||||
|
val isConnected = d != null && d.connectionState == 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.remove(this);
|
||||||
StateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
StateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
||||||
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo);
|
||||||
|
@ -156,6 +225,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { setLoading(connectionState != CastConnectionState.CONNECTED); };
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { setLoading(connectionState != CastConnectionState.CONNECTED); };
|
||||||
updateDevice();
|
updateDevice();
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
updateDevice();
|
updateDevice();
|
||||||
}
|
}
|
||||||
|
@ -167,9 +237,64 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||||
_device = null;
|
_device = null;
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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_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) {
|
if (d is ChromecastCastingDevice) {
|
||||||
|
@ -180,7 +305,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
_textType.text = "AirPlay";
|
_textType.text = "AirPlay";
|
||||||
} else if (d is FCastCastingDevice) {
|
} else if (d is FCastCastingDevice) {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
_imageDevice.setImageResource(R.drawable.ic_fc);
|
||||||
_textType.text = "FastCast";
|
_textType.text = "FCast";
|
||||||
}
|
}
|
||||||
|
|
||||||
_textName.text = d.name;
|
_textName.text = d.name;
|
||||||
|
@ -220,6 +345,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun enableControls(views: List<View>) {
|
private fun enableControls(views: List<View>) {
|
||||||
views.forEach { enableControl(it) }
|
views.forEach { enableControl(it) }
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventHandler = EventHandler()
|
||||||
|
val device: RsCastingDevice
|
||||||
|
|
||||||
|
var usedRemoteAddress: InetAddress? = null
|
||||||
|
var localAddress: InetAddress? = null
|
||||||
|
var connectionState = CastConnectionState.DISCONNECTED
|
||||||
|
var volume: Double = 0.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("CastingDeviceHandle", "Stopping device")
|
||||||
|
device.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e("CastingDeviceHandle", "Failed to stop device: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadVideo(
|
||||||
|
streamType: String,
|
||||||
|
contentType: String,
|
||||||
|
contentId: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
device.loadVideo(contentType, contentId, resumePosition, speed)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e("CastingDevice", "Failed to load video: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadContent(
|
||||||
|
contentType: String,
|
||||||
|
content: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?
|
||||||
|
) {
|
||||||
|
device.loadContent(contentType, content, resumePosition, duration, speed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
@ -98,6 +98,7 @@ 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.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
|
||||||
|
@ -175,6 +176,7 @@ 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
|
||||||
|
@ -664,6 +666,50 @@ 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;
|
||||||
|
@ -683,9 +729,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
updatePillButtonVisibilities();
|
|
||||||
|
|
||||||
StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
||||||
val activeDevice = StateCasting.instance.activeDevice;
|
val activeDevice = StateCasting.instance.activeDevice;
|
||||||
|
@ -708,6 +752,9 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_timeBar.setDuration(video?.duration ?: 0);
|
_timeBar.setDuration(video?.duration ?: 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePillButtonVisibilities();
|
||||||
|
|
||||||
_cast.onTimeJobTimeChanged_s.subscribe {
|
_cast.onTimeJobTimeChanged_s.subscribe {
|
||||||
if (_isCasting) {
|
if (_isCasting) {
|
||||||
|
@ -1188,8 +1235,13 @@ class VideoDetailView : ConstraintLayout {
|
||||||
_onPauseCalled = true;
|
_onPauseCalled = true;
|
||||||
_taskLoadVideo.cancel();
|
_taskLoadVideo.cancel();
|
||||||
|
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
if(ExpStateCasting.instance.isCasting)
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
if(StateCasting.instance.isCasting)
|
if(StateCasting.instance.isCasting)
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(isAudioOnlyUserAction)
|
if(isAudioOnlyUserAction)
|
||||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||||
|
@ -1240,9 +1292,15 @@ 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);
|
||||||
|
@ -1974,7 +2032,11 @@ class VideoDetailView : ConstraintLayout {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
val isCasting = StateCasting.instance.isCasting
|
val isCasting = if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.isCasting
|
||||||
|
} else {
|
||||||
|
StateCasting.instance.isCasting
|
||||||
|
}
|
||||||
if (!isCasting) {
|
if (!isCasting) {
|
||||||
setCastEnabled(false);
|
setCastEnabled(false);
|
||||||
|
|
||||||
|
@ -2051,11 +2113,19 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
|
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
|
||||||
try {
|
try {
|
||||||
val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
|
val castingSucceeded = if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
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) {
|
||||||
|
@ -2250,7 +2320,13 @@ class VideoDetailView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentPlaybackRate = (if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()) ?: 1.0
|
val currentPlaybackRate = (if (_isCasting) {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.activeDevice?.speed
|
||||||
|
} else {
|
||||||
|
StateCasting.instance.activeDevice?.speed
|
||||||
|
}
|
||||||
|
} 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())
|
||||||
};
|
};
|
||||||
|
@ -2368,8 +2444,18 @@ class VideoDetailView : ConstraintLayout {
|
||||||
?.distinct()
|
?.distinct()
|
||||||
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
||||||
|
|
||||||
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
val canSetSpeed = !_isCasting || if (Settings.instance.casting.experimentalCasting) {
|
||||||
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
ExpStateCasting.instance.activeDevice?.device?.supportsFeature(DeviceFeature.SET_SPEED) == true
|
||||||
|
} else {
|
||||||
|
StateCasting.instance.activeDevice?.canSetSpeed == true
|
||||||
|
}
|
||||||
|
val currentPlaybackRate = if (_isCasting) {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.activeDevice?.speed
|
||||||
|
} else {
|
||||||
|
StateCasting.instance.activeDevice?.speed
|
||||||
|
}
|
||||||
|
} else _player.getPlaybackRate()
|
||||||
val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null;
|
val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null;
|
||||||
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
||||||
R.string.quality), null, true,
|
R.string.quality), null, true,
|
||||||
|
@ -2383,7 +2469,13 @@ 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) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate();
|
val currentPlaybackSpeed = if (_isCasting) {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.activeDevice?.speed
|
||||||
|
} else {
|
||||||
|
StateCasting.instance.activeDevice?.speed
|
||||||
|
}
|
||||||
|
} else _player.getPlaybackRate();
|
||||||
var playbackSpeedString = v;
|
var playbackSpeedString = v;
|
||||||
val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep();
|
val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep();
|
||||||
if(v == "+")
|
if(v == "+")
|
||||||
|
@ -2392,6 +2484,16 @@ class VideoDetailView : ConstraintLayout {
|
||||||
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) {
|
if (_isCasting) {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
val ad = ExpStateCasting.instance.activeDevice ?: return@subscribe
|
||||||
|
if (!ad.device.supportsFeature(DeviceFeature.SET_SPEED)) {
|
||||||
|
return@subscribe
|
||||||
|
}
|
||||||
|
|
||||||
|
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
||||||
|
ad.device.changeSpeed(newPlaybackSpeed)
|
||||||
|
setSelected(playbackSpeedString);
|
||||||
|
} else {
|
||||||
val ad = StateCasting.instance.activeDevice ?: return@subscribe
|
val ad = StateCasting.instance.activeDevice ?: return@subscribe
|
||||||
if (!ad.canSetSpeed) {
|
if (!ad.canSetSpeed) {
|
||||||
return@subscribe
|
return@subscribe
|
||||||
|
@ -2400,6 +2502,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
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)})");
|
||||||
ad.changeSpeed(newPlaybackSpeed)
|
ad.changeSpeed(newPlaybackSpeed)
|
||||||
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)})");
|
||||||
_player.setPlaybackRate(playbackSpeedString.toFloat());
|
_player.setPlaybackRate(playbackSpeedString.toFloat());
|
||||||
|
@ -2514,9 +2617,15 @@ class VideoDetailView : ConstraintLayout {
|
||||||
//Handlers
|
//Handlers
|
||||||
private fun handlePlay() {
|
private fun handlePlay() {
|
||||||
Logger.i(TAG, "handlePlay")
|
Logger.i(TAG, "handlePlay")
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
if (!ExpStateCasting.instance.resumeVideo()) {
|
||||||
|
_player.play()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (!StateCasting.instance.resumeVideo()) {
|
if (!StateCasting.instance.resumeVideo()) {
|
||||||
_player.play();
|
_player.play();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
onShouldEnterPictureInPictureChanged.emit()
|
onShouldEnterPictureInPictureChanged.emit()
|
||||||
|
|
||||||
//TODO: This was needed because handleLowerVolume was done.
|
//TODO: This was needed because handleLowerVolume was done.
|
||||||
|
@ -2531,27 +2640,54 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
private fun handlePause() {
|
private fun handlePause() {
|
||||||
Logger.i(TAG, "handlePause")
|
Logger.i(TAG, "handlePause")
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
if (!ExpStateCasting.instance.pauseVideo()) {
|
||||||
|
_player.pause()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (!StateCasting.instance.pauseVideo()) {
|
if (!StateCasting.instance.pauseVideo()) {
|
||||||
_player.pause();
|
_player.pause()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onShouldEnterPictureInPictureChanged.emit()
|
onShouldEnterPictureInPictureChanged.emit()
|
||||||
}
|
}
|
||||||
private fun handleSeek(ms: Long) {
|
private fun handleSeek(ms: Long) {
|
||||||
Logger.i(TAG, "handleSeek(ms=$ms)")
|
Logger.i(TAG, "handleSeek(ms=$ms)")
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
if (!ExpStateCasting.instance.videoSeekTo(ms.toDouble() / 1000.0)) {
|
||||||
|
_player.seekTo(ms)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (!StateCasting.instance.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 (Settings.instance.casting.experimentalCasting) {
|
||||||
|
if (!ExpStateCasting.instance.stopVideo()) {
|
||||||
|
_player.stop()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (!StateCasting.instance.stopVideo()) {
|
if (!StateCasting.instance.stopVideo()) {
|
||||||
_player.stop();
|
_player.stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePlayChanged(playing: Boolean) {
|
private fun handlePlayChanged(playing: Boolean) {
|
||||||
Logger.i(TAG, "handlePlayChanged(playing=$playing)")
|
Logger.i(TAG, "handlePlayChanged(playing=$playing)")
|
||||||
|
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
val ad = ExpStateCasting.instance.activeDevice;
|
||||||
|
if (ad != null) {
|
||||||
|
_cast.setIsPlaying(playing);
|
||||||
|
} else {
|
||||||
|
StatePlayer.instance.updateMediaSession( null);
|
||||||
|
StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, _player.exoPlayer?.player?.currentPosition ?: 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
val ad = StateCasting.instance.activeDevice;
|
val ad = StateCasting.instance.activeDevice;
|
||||||
if (ad != null) {
|
if (ad != null) {
|
||||||
_cast.setIsPlaying(playing);
|
_cast.setIsPlaying(playing);
|
||||||
|
@ -2559,6 +2695,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
StatePlayer.instance.updateMediaSession( null);
|
StatePlayer.instance.updateMediaSession( null);
|
||||||
StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, _player.exoPlayer?.player?.currentPosition ?: 0);
|
StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, _player.exoPlayer?.player?.currentPosition ?: 0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(playing) {
|
if(playing) {
|
||||||
_minimize_controls_pause.visibility = View.VISIBLE;
|
_minimize_controls_pause.visibility = View.VISIBLE;
|
||||||
|
@ -2595,11 +2732,27 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
val d = ExpStateCasting.instance.activeDevice;
|
||||||
|
if (d != null && d.connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED)
|
||||||
|
castIfAvailable(
|
||||||
|
context.contentResolver,
|
||||||
|
video,
|
||||||
|
videoSource,
|
||||||
|
_lastAudioSource,
|
||||||
|
_lastSubtitleSource,
|
||||||
|
(d.expectedCurrentTime * 1000.0).toLong(),
|
||||||
|
d.speed
|
||||||
|
);
|
||||||
|
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
|
||||||
|
_player.hideControls(false); //TODO: Disable player?
|
||||||
|
} else {
|
||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||||
castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
||||||
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
|
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
|
||||||
_player.hideControls(false); //TODO: Disable player?
|
_player.hideControls(false); //TODO: Disable player?
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "handleSelectVideoTrack failed", e)
|
Logger.e(TAG, "handleSelectVideoTrack failed", e)
|
||||||
}
|
}
|
||||||
|
@ -2616,11 +2769,35 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
val d = StateCasting.instance.activeDevice;
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
val d = ExpStateCasting.instance.activeDevice;
|
||||||
castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed)
|
if (d != null && d.connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED)
|
||||||
|
castIfAvailable(
|
||||||
|
context.contentResolver,
|
||||||
|
video,
|
||||||
|
_lastVideoSource,
|
||||||
|
audioSource,
|
||||||
|
_lastSubtitleSource,
|
||||||
|
(d.expectedCurrentTime * 1000.0).toLong(),
|
||||||
|
d.speed
|
||||||
|
)
|
||||||
else if (!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
|
else if (!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
|
||||||
_player.hideControls(false); //TODO: Disable player?
|
_player.hideControls(false); //TODO: Disable player?
|
||||||
|
} else {
|
||||||
|
val d = StateCasting.instance.activeDevice;
|
||||||
|
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||||
|
castIfAvailable(
|
||||||
|
context.contentResolver,
|
||||||
|
video,
|
||||||
|
_lastVideoSource,
|
||||||
|
audioSource,
|
||||||
|
_lastSubtitleSource,
|
||||||
|
(d.expectedCurrentTime * 1000.0).toLong(),
|
||||||
|
d.speed
|
||||||
|
)
|
||||||
|
else if (!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
|
||||||
|
_player.hideControls(false); //TODO: Disable player?
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "handleSelectAudioTrack failed", e)
|
Logger.e(TAG, "handleSelectAudioTrack failed", e)
|
||||||
}
|
}
|
||||||
|
@ -2638,12 +2815,37 @@ class VideoDetailView : ConstraintLayout {
|
||||||
|
|
||||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
val d = StateCasting.instance.activeDevice;
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
val d = ExpStateCasting.instance.activeDevice;
|
||||||
castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
|
if (d != null && d.connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED)
|
||||||
|
castIfAvailable(
|
||||||
|
context.contentResolver,
|
||||||
|
video,
|
||||||
|
_lastVideoSource,
|
||||||
|
_lastAudioSource,
|
||||||
|
toSet,
|
||||||
|
(d.expectedCurrentTime * 1000.0).toLong(),
|
||||||
|
d.speed
|
||||||
|
);
|
||||||
else {
|
else {
|
||||||
_player.swapSubtitles(toSet);
|
_player.swapSubtitles(toSet);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
val d = StateCasting.instance.activeDevice;
|
||||||
|
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
|
||||||
|
castIfAvailable(
|
||||||
|
context.contentResolver,
|
||||||
|
video,
|
||||||
|
_lastVideoSource,
|
||||||
|
_lastAudioSource,
|
||||||
|
toSet,
|
||||||
|
(d.expectedCurrentTime * 1000.0).toLong(),
|
||||||
|
d.speed
|
||||||
|
);
|
||||||
|
else {
|
||||||
|
_player.swapSubtitles(toSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "handleSelectSubtitleTrack failed", e)
|
Logger.e(TAG, "handleSelectSubtitleTrack failed", e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
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 {
|
||||||
|
@ -16,3 +17,18 @@ class CastingDeviceInfo {
|
||||||
this.port = port;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,14 +6,34 @@ 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
|
||||||
|
|
||||||
data class DeviceAdapterEntry(val castingDevice: CastingDevice, val isPinnedDevice: Boolean, val isOnlineDevice: Boolean)
|
sealed class GenericCastingDevice {
|
||||||
|
class Normal(val device: CastingDevice): GenericCastingDevice()
|
||||||
|
class Experimental(val handle: CastingDeviceHandle): GenericCastingDevice()
|
||||||
|
|
||||||
|
fun name(): String? {
|
||||||
|
return when (this) {
|
||||||
|
is Experimental -> this.handle.device.name()
|
||||||
|
is Normal -> this.device.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isReady(): Boolean {
|
||||||
|
return when(this) {
|
||||||
|
is Experimental -> this.handle.device.isReady()
|
||||||
|
is Normal -> this.device.isReady
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeviceAdapterEntry(val castingDevice: GenericCastingDevice, val isPinnedDevice: Boolean, val isOnlineDevice: Boolean)
|
||||||
|
|
||||||
class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
|
class DeviceAdapter : RecyclerView.Adapter<DeviceViewHolder> {
|
||||||
private val _devices: List<DeviceAdapterEntry>;
|
private val _devices: List<DeviceAdapterEntry>;
|
||||||
|
|
||||||
var onPin = Event1<CastingDevice>();
|
var onPin = Event1<GenericCastingDevice>();
|
||||||
var onConnect = Event1<CastingDevice>();
|
var onConnect = Event1<GenericCastingDevice>();
|
||||||
|
|
||||||
constructor(devices: List<DeviceAdapterEntry>) : super() {
|
constructor(devices: List<DeviceAdapterEntry>) : super() {
|
||||||
_devices = devices;
|
_devices = devices;
|
||||||
|
|
|
@ -4,21 +4,20 @@ import android.graphics.drawable.Animatable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
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.UIDialogs
|
||||||
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.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.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||||
import androidx.core.view.isVisible
|
import org.fcast.sender_sdk.ProtocolType
|
||||||
import com.futo.platformplayer.UIDialogs
|
|
||||||
|
|
||||||
class DeviceViewHolder : ViewHolder {
|
class DeviceViewHolder : ViewHolder {
|
||||||
private val _layoutDevice: FrameLayout;
|
private val _layoutDevice: FrameLayout;
|
||||||
|
@ -32,11 +31,11 @@ class DeviceViewHolder : ViewHolder {
|
||||||
private var _animatableLoader: Animatable? = null;
|
private var _animatableLoader: Animatable? = null;
|
||||||
private var _imagePin: ImageView;
|
private var _imagePin: ImageView;
|
||||||
|
|
||||||
var device: CastingDevice? = null
|
var device: GenericCastingDevice? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var onPin = Event1<CastingDevice>();
|
var onPin = Event1<GenericCastingDevice>();
|
||||||
val onConnect = Event1<CastingDevice>();
|
val onConnect = Event1<GenericCastingDevice>();
|
||||||
|
|
||||||
constructor(view: View) : super(view) {
|
constructor(view: View) : super(view) {
|
||||||
_root = view.findViewById(R.id.layout_root);
|
_root = view.findViewById(R.id.layout_root);
|
||||||
|
@ -56,9 +55,12 @@ class DeviceViewHolder : ViewHolder {
|
||||||
|
|
||||||
val connect = {
|
val connect = {
|
||||||
device?.let { dev ->
|
device?.let { dev ->
|
||||||
if (dev.isReady) {
|
when (dev) {
|
||||||
|
is GenericCastingDevice.Normal -> {
|
||||||
|
if (dev.device.isReady) {
|
||||||
|
// NOTE: we assume normal casting is used
|
||||||
StateCasting.instance.activeDevice?.stopCasting()
|
StateCasting.instance.activeDevice?.stopCasting()
|
||||||
StateCasting.instance.connectDevice(dev)
|
StateCasting.instance.connectDevice(dev.device)
|
||||||
onConnect.emit(dev)
|
onConnect.emit(dev)
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
@ -68,6 +70,22 @@ class DeviceViewHolder : ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is GenericCastingDevice.Experimental -> {
|
||||||
|
if (dev.handle.device.isReady()) {
|
||||||
|
// NOTE: we assume experimental casting is used
|
||||||
|
ExpStateCasting.instance.activeDevice?.device?.stopCasting()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_textName.setOnClickListener { connect() };
|
_textName.setOnClickListener { connect() };
|
||||||
|
@ -80,22 +98,26 @@ class DeviceViewHolder : ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
// fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
||||||
if (d is ChromecastCastingDevice) {
|
|
||||||
|
fun bind(d: GenericCastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
||||||
|
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 is AirPlayCastingDevice) {
|
} else if (d.device is AirPlayCastingDevice) {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||||
_textType.text = "AirPlay";
|
_textType.text = "AirPlay";
|
||||||
} else if (d is FCastCastingDevice) {
|
} else if (d.device is FCastCastingDevice) {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
_imageDevice.setImageResource(R.drawable.ic_fc);
|
||||||
_textType.text = "FCast";
|
_textType.text = "FCast";
|
||||||
}
|
}
|
||||||
|
|
||||||
_textName.text = d.name;
|
_textName.text = d.device.name;
|
||||||
_imageOnline.visibility = if (isOnlineDevice && d.isReady) View.VISIBLE else View.GONE
|
_imageOnline.visibility = if (isOnlineDevice && d.device.isReady) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
if (!d.isReady) {
|
if (!d.device.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;
|
||||||
|
@ -114,7 +136,63 @@ class DeviceViewHolder : ViewHolder {
|
||||||
_imagePin.visibility = View.VISIBLE;
|
_imagePin.visibility = View.VISIBLE;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (d.isReady) {
|
if (d.device.isReady) {
|
||||||
|
_imageLoader.visibility = View.GONE;
|
||||||
|
_textNotReady.visibility = View.GONE;
|
||||||
|
_imagePin.visibility = View.VISIBLE;
|
||||||
|
} else {
|
||||||
|
_imageLoader.visibility = View.GONE;
|
||||||
|
_textNotReady.visibility = View.VISIBLE;
|
||||||
|
_imagePin.visibility = View.VISIBLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_imagePin.setImageResource(if (isPinnedDevice) R.drawable.keep_24px else R.drawable.ic_pin)
|
||||||
|
|
||||||
|
if (_imageLoader.isVisible) {
|
||||||
|
_animatableLoader?.start();
|
||||||
|
} else {
|
||||||
|
_animatableLoader?.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
device = d;
|
||||||
|
}
|
||||||
|
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_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 = StateCasting.instance.activeDevice;
|
||||||
|
if (dev == d) {
|
||||||
|
if (dev.connectionState == CastConnectionState.CONNECTED) {
|
||||||
|
_imageLoader.visibility = View.GONE;
|
||||||
|
_textNotReady.visibility = View.GONE;
|
||||||
|
_imagePin.visibility = View.VISIBLE;
|
||||||
|
} else {
|
||||||
|
_imageLoader.visibility = View.VISIBLE;
|
||||||
|
_textNotReady.visibility = View.GONE;
|
||||||
|
_imagePin.visibility = View.VISIBLE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (d.handle.device.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;
|
||||||
|
@ -137,3 +215,5 @@ class DeviceViewHolder : ViewHolder {
|
||||||
device = d;
|
device = d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
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>>();
|
||||||
|
@ -27,9 +28,15 @@ 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();
|
||||||
}
|
}
|
||||||
|
@ -37,6 +44,23 @@ 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);
|
||||||
|
@ -53,9 +77,11 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
||||||
setColorFilter(inactiveColor);
|
setColorFilter(inactiveColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
setOnClickListener(null);
|
setOnClickListener(null);
|
||||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||||
|
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -27,6 +27,7 @@ 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.formatDuration
|
import com.futo.platformplayer.formatDuration
|
||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
|
@ -38,6 +39,7 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
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;
|
||||||
|
@ -96,22 +98,48 @@ 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 = ExpStateCasting.instance.activeDevice ?: return@subscribe;
|
||||||
|
_speedHoldWasPlaying = d.isPlaying
|
||||||
|
_speedHoldPrevRate = d.speed
|
||||||
|
if (d.device.supportsFeature(DeviceFeature.SET_SPEED)) {
|
||||||
|
d.device.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
||||||
|
}
|
||||||
|
d.device.resumePlayback()
|
||||||
|
} else {
|
||||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||||
_speedHoldWasPlaying = d.isPlaying
|
_speedHoldWasPlaying = d.isPlaying
|
||||||
_speedHoldPrevRate = d.speed
|
_speedHoldPrevRate = d.speed
|
||||||
if (d.canSetSpeed)
|
if (d.canSetSpeed) {
|
||||||
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
||||||
|
}
|
||||||
d.resumeVideo()
|
d.resumeVideo()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_gestureControlView.onSpeedHoldEnd.subscribe {
|
_gestureControlView.onSpeedHoldEnd.subscribe {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
val d = ExpStateCasting.instance.activeDevice ?: return@subscribe;
|
||||||
|
if (!_speedHoldWasPlaying) {
|
||||||
|
d.device.resumePlayback()
|
||||||
|
}
|
||||||
|
d.device.changeSpeed(_speedHoldPrevRate)
|
||||||
|
} else {
|
||||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||||
if (!_speedHoldWasPlaying) d.pauseVideo()
|
if (!_speedHoldWasPlaying) {
|
||||||
|
d.pauseVideo()
|
||||||
|
}
|
||||||
d.changeSpeed(_speedHoldPrevRate)
|
d.changeSpeed(_speedHoldPrevRate)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_gestureControlView.onSeek.subscribe {
|
_gestureControlView.onSeek.subscribe {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
val d = ExpStateCasting.instance.activeDevice ?: return@subscribe;
|
||||||
|
ExpStateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
|
||||||
|
} else {
|
||||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||||
StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
|
StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonLoop.setOnClickListener {
|
_buttonLoop.setOnClickListener {
|
||||||
|
@ -122,22 +150,46 @@ 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) {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.videoSeekTo(position.toDouble());
|
||||||
|
} else {
|
||||||
StateCasting.instance.videoSeekTo(position.toDouble());
|
StateCasting.instance.videoSeekTo(position.toDouble());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.videoSeekTo(position.toDouble());
|
||||||
|
} else {
|
||||||
StateCasting.instance.videoSeekTo(position.toDouble());
|
StateCasting.instance.videoSeekTo(position.toDouble());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.videoSeekTo(position.toDouble());
|
||||||
|
} else {
|
||||||
StateCasting.instance.videoSeekTo(position.toDouble());
|
StateCasting.instance.videoSeekTo(position.toDouble());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_buttonMinimize.setOnClickListener { onMinimizeClick.emit(); };
|
_buttonMinimize.setOnClickListener { onMinimizeClick.emit(); };
|
||||||
_buttonSettings.setOnClickListener { onSettingsClick.emit(); };
|
_buttonSettings.setOnClickListener { onSettingsClick.emit(); };
|
||||||
_buttonPlay.setOnClickListener { StateCasting.instance.resumeVideo(); };
|
_buttonPlay.setOnClickListener {
|
||||||
_buttonPause.setOnClickListener { StateCasting.instance.pauseVideo(); };
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.resumeVideo()
|
||||||
|
} else {
|
||||||
|
StateCasting.instance.resumeVideo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_buttonPause.setOnClickListener {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.pauseVideo()
|
||||||
|
} else {
|
||||||
|
StateCasting.instance.pauseVideo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isInEditMode) {
|
if (!isInEditMode) {
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
|
@ -220,6 +272,8 @@ class CastView : ConstraintLayout {
|
||||||
stopTimeJob()
|
stopTimeJob()
|
||||||
|
|
||||||
if(isPlaying) {
|
if(isPlaying) {
|
||||||
|
// NOTE: the experimental implementation polls automatically
|
||||||
|
if (!Settings.instance.casting.experimentalCasting) {
|
||||||
val d = StateCasting.instance.activeDevice;
|
val d = StateCasting.instance.activeDevice;
|
||||||
if (d is AirPlayCastingDevice || d is ChromecastCastingDevice) {
|
if (d is AirPlayCastingDevice || d is ChromecastCastingDevice) {
|
||||||
_updateTimeJob = _scope.launch {
|
_updateTimeJob = _scope.launch {
|
||||||
|
@ -236,6 +290,7 @@ class CastView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!_inPictureInPicture) {
|
if (!_inPictureInPicture) {
|
||||||
_buttonPause.visibility = View.VISIBLE;
|
_buttonPause.visibility = View.VISIBLE;
|
||||||
|
@ -247,7 +302,11 @@ class CastView : ConstraintLayout {
|
||||||
_buttonPlay.visibility = View.VISIBLE;
|
_buttonPlay.visibility = View.VISIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
val position = StateCasting.instance.activeDevice?.expectedCurrentTime?.times(1000.0)?.toLong();
|
val position = if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
ExpStateCasting.instance.activeDevice?.expectedCurrentTime?.times(1000.0)?.toLong()
|
||||||
|
} else {
|
||||||
|
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));
|
||||||
|
@ -311,6 +370,14 @@ class CastView : ConstraintLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPlaybackStateCompat(): Int {
|
private fun getPlaybackStateCompat(): Int {
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
val d = ExpStateCasting.instance.activeDevice ?: return PlaybackState.STATE_NONE;
|
||||||
|
|
||||||
|
return when(d.isPlaying) {
|
||||||
|
true -> PlaybackStateCompat.STATE_PLAYING;
|
||||||
|
else -> PlaybackStateCompat.STATE_PAUSED;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
val d = StateCasting.instance.activeDevice ?: return PlaybackState.STATE_NONE;
|
val d = StateCasting.instance.activeDevice ?: return PlaybackState.STATE_NONE;
|
||||||
|
|
||||||
return when(d.isPlaying) {
|
return when(d.isPlaying) {
|
||||||
|
@ -318,6 +385,7 @@ class CastView : ConstraintLayout {
|
||||||
else -> PlaybackStateCompat.STATE_PAUSED;
|
else -> PlaybackStateCompat.STATE_PAUSED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setLoading(isLoading: Boolean) {
|
fun setLoading(isLoading: Boolean) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|
|
@ -82,6 +82,8 @@
|
||||||
<string name="allow_ipv6_description">If casting over IPV6 is allowed, can cause issues on some networks</string>
|
<string name="allow_ipv6_description">If casting over IPV6 is allowed, can cause issues on some networks</string>
|
||||||
<string name="allow_ipv4">Allow Link Local IPV4</string>
|
<string name="allow_ipv4">Allow Link Local IPV4</string>
|
||||||
<string name="allow_ipv4_description">If casting over IPV4 link local is allowed, can cause issues on some networks</string>
|
<string name="allow_ipv4_description">If casting over IPV4 link local is allowed, can cause issues on some networks</string>
|
||||||
|
<string name="experimental_cast">Experimental</string>
|
||||||
|
<string name="experimental_cast_description">Use experimental casting backend (requires restart)</string>
|
||||||
<string name="discover">Discover</string>
|
<string name="discover">Discover</string>
|
||||||
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
|
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
|
||||||
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
|
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue