mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-09-12 04:22:37 +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"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
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)
|
||||
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?
|
||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||
@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.ProgressDialog
|
||||
import com.futo.platformplayer.engine.exceptions.PluginException
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||
|
@ -437,6 +438,32 @@ class UIDialogs {
|
|||
|
||||
|
||||
fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
val d = ExpStateCasting.instance.activeDevice;
|
||||
if (d != null) {
|
||||
val dialog = ConnectedCastingDialog(context);
|
||||
if (context is Activity) {
|
||||
dialog.setOwnerActivity(context)
|
||||
}
|
||||
registerDialogOpened(dialog);
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
} else {
|
||||
val dialog = ConnectCastingDialog(context);
|
||||
if (context is Activity) {
|
||||
dialog.setOwnerActivity(context)
|
||||
}
|
||||
registerDialogOpened(dialog);
|
||||
val c = context
|
||||
if (c is Activity) {
|
||||
dialog.setOwnerActivity(c);
|
||||
}
|
||||
ownerActivity?.let { dialog.setOwnerActivity(it) }
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
}
|
||||
} else {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d != null) {
|
||||
val dialog = ConnectedCastingDialog(context);
|
||||
|
@ -462,6 +489,7 @@ class UIDialogs {
|
|||
dialog.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
|
||||
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.constructs.Event1
|
||||
import com.futo.platformplayer.dp
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ArticleDetailFragment
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
||||
|
@ -507,7 +508,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
handleIntent(intent);
|
||||
|
||||
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 {
|
||||
|
@ -1046,7 +1051,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||
Logger.i(TAG, "handleFCast");
|
||||
|
||||
try {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.handleUrl(this, url)
|
||||
} else {
|
||||
StateCasting.instance.handleUrl(this, url)
|
||||
}
|
||||
return true;
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
||||
|
|
|
@ -8,9 +8,11 @@ import android.view.View
|
|||
import android.view.WindowManager
|
||||
import android.widget.*
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.casting.CastProtocolType
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||
import com.futo.platformplayer.toInetAddress
|
||||
|
||||
|
@ -101,7 +103,11 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||
|
||||
_textError.visibility = View.GONE;
|
||||
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();
|
||||
};
|
||||
|
||||
|
|
|
@ -15,15 +15,18 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.CastingDevice
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.views.adapters.DeviceAdapter
|
||||
import com.futo.platformplayer.views.adapters.DeviceAdapterEntry
|
||||
import com.futo.platformplayer.views.adapters.GenericCastingDevice
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -55,17 +58,33 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
_recyclerDevices.layoutManager = LinearLayoutManager(context);
|
||||
|
||||
_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
|
||||
if (newIsRemembered) {
|
||||
StateCasting.instance.addRememberedDevice(d)
|
||||
val name = d.name
|
||||
ExpStateCasting.instance.addRememberedDevice(d.handle)
|
||||
val name = d.handle.device.name()
|
||||
_rememberedDevices.add(name)
|
||||
} else {
|
||||
ExpStateCasting.instance.removeRememberedDevice(d.handle)
|
||||
_rememberedDevices.remove(d.handle.device.name())
|
||||
}
|
||||
}
|
||||
is GenericCastingDevice.Normal -> {
|
||||
val isRemembered = _rememberedDevices.contains(d.device.name)
|
||||
val newIsRemembered = !isRemembered
|
||||
if (newIsRemembered) {
|
||||
StateCasting.instance.addRememberedDevice(d.device)
|
||||
val name = d.device.name
|
||||
if (name != null) {
|
||||
_rememberedDevices.add(name)
|
||||
}
|
||||
} else {
|
||||
StateCasting.instance.removeRememberedDevice(d)
|
||||
_rememberedDevices.remove(d.name)
|
||||
StateCasting.instance.removeRememberedDevice(d.device)
|
||||
_rememberedDevices.remove(d.device.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
updateUnifiedList()
|
||||
}
|
||||
|
@ -105,13 +124,48 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
|
||||
(_imageLoader.drawable as Animatable?)?.start();
|
||||
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
synchronized(ExpStateCasting.instance.devices) {
|
||||
_devices.addAll(ExpStateCasting.instance.devices.values.map { it.device.name() })
|
||||
}
|
||||
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
||||
} else {
|
||||
synchronized(StateCasting.instance.devices) {
|
||||
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
|
||||
}
|
||||
|
||||
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
||||
}
|
||||
|
||||
updateUnifiedList()
|
||||
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
||||
_devices.add(d.name())
|
||||
updateUnifiedList()
|
||||
}
|
||||
|
||||
ExpStateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
||||
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name() == d.device.name() }
|
||||
if (index != -1) {
|
||||
val dev = GenericCastingDevice.Experimental(d)
|
||||
_unifiedDevices[index] = DeviceAdapterEntry(dev, _unifiedDevices[index].isPinnedDevice, _unifiedDevices[index].isOnlineDevice)
|
||||
_adapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
ExpStateCasting.instance.onDeviceRemoved.subscribe(this) { deviceName ->
|
||||
_devices.remove(deviceName)
|
||||
updateUnifiedList()
|
||||
}
|
||||
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, connectionState ->
|
||||
if (connectionState == com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
||||
val name = d.name
|
||||
if (name != null)
|
||||
|
@ -120,9 +174,13 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
}
|
||||
|
||||
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
||||
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name == d.name }
|
||||
val index = _unifiedDevices.indexOfFirst { it.castingDevice.name() == d.name }
|
||||
if (index != -1) {
|
||||
_unifiedDevices[index] = DeviceAdapterEntry(d, _unifiedDevices[index].isPinnedDevice, _unifiedDevices[index].isOnlineDevice)
|
||||
_unifiedDevices[index] = DeviceAdapterEntry(
|
||||
GenericCastingDevice.Normal(d),
|
||||
_unifiedDevices[index].isPinnedDevice,
|
||||
_unifiedDevices[index].isOnlineDevice
|
||||
)
|
||||
_adapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
@ -140,6 +198,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
super.dismiss()
|
||||
|
@ -160,16 +219,16 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList[oldItemPosition]
|
||||
val newItem = newList[newItemPosition]
|
||||
return oldItem.castingDevice.name == newItem.castingDevice.name
|
||||
&& oldItem.castingDevice.isReady == newItem.castingDevice.isReady
|
||||
return oldItem.castingDevice.name() == newItem.castingDevice.name()
|
||||
&& oldItem.castingDevice.isReady() == newItem.castingDevice.isReady()
|
||||
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
|
||||
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
|
||||
}
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList[oldItemPosition]
|
||||
val newItem = newList[newItemPosition]
|
||||
return oldItem.castingDevice.name == newItem.castingDevice.name
|
||||
&& oldItem.castingDevice.isReady == newItem.castingDevice.isReady
|
||||
return oldItem.castingDevice.name() == newItem.castingDevice.name()
|
||||
&& oldItem.castingDevice.isReady() == newItem.castingDevice.isReady()
|
||||
&& oldItem.isOnlineDevice == newItem.isOnlineDevice
|
||||
&& oldItem.isPinnedDevice == newItem.isPinnedDevice
|
||||
}
|
||||
|
@ -184,24 +243,67 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
}
|
||||
|
||||
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>()
|
||||
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
val onlineDevices = ExpStateCasting.instance.devices.values.associateBy { it.device.name() }
|
||||
val rememberedDevices = ExpStateCasting.instance.getRememberedCastingDevices().associateBy { it.device.name() }
|
||||
|
||||
val intersectionNames = _devices.intersect(_rememberedDevices)
|
||||
for (name in intersectionNames) {
|
||||
onlineDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, true, true)) }
|
||||
onlineDevices[name]?.let {
|
||||
unifiedList.add(DeviceAdapterEntry(
|
||||
GenericCastingDevice.Experimental(it), true, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val onlineOnlyNames = _devices - _rememberedDevices
|
||||
for (name in onlineOnlyNames) {
|
||||
onlineDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(it, false, true)) }
|
||||
onlineDevices[name]?.let {
|
||||
unifiedList.add(DeviceAdapterEntry(
|
||||
GenericCastingDevice.Experimental(it), false, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val rememberedOnlyNames = _rememberedDevices - _devices
|
||||
for (name in rememberedOnlyNames) {
|
||||
rememberedDevices[name]?.let { unifiedList.add(DeviceAdapterEntry(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
|
||||
|
|
|
@ -12,6 +12,7 @@ import android.widget.ImageView
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
||||
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.FCastCastingDevice
|
||||
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.logging.Logger
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.fcast.sender_sdk.DeviceFeature
|
||||
import org.fcast.sender_sdk.ProtocolType
|
||||
|
||||
class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
||||
private lateinit var _buttonClose: Button;
|
||||
|
@ -69,19 +73,31 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
|
||||
_buttonPlay = findViewById(R.id.button_play);
|
||||
_buttonPlay.setOnClickListener {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.activeDevice?.device?.resumePlayback()
|
||||
} else {
|
||||
StateCasting.instance.activeDevice?.resumeVideo()
|
||||
}
|
||||
}
|
||||
|
||||
_buttonPause = findViewById(R.id.button_pause);
|
||||
_buttonPause.setOnClickListener {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.activeDevice?.device?.pausePlayback()
|
||||
} else {
|
||||
StateCasting.instance.activeDevice?.pauseVideo()
|
||||
}
|
||||
}
|
||||
|
||||
_buttonStop = findViewById(R.id.button_stop);
|
||||
_buttonStop.setOnClickListener {
|
||||
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.activeDevice?.device?.stopPlayback()
|
||||
} else {
|
||||
StateCasting.instance.activeDevice?.stopVideo()
|
||||
}
|
||||
}
|
||||
|
||||
_buttonNext = findViewById(R.id.button_next);
|
||||
_buttonNext.setOnClickListener {
|
||||
|
@ -90,7 +106,11 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
|
||||
_buttonClose.setOnClickListener { dismiss(); };
|
||||
_buttonDisconnect.setOnClickListener {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.activeDevice?.device?.stopCasting()
|
||||
} else {
|
||||
StateCasting.instance.activeDevice?.stopCasting();
|
||||
}
|
||||
dismiss();
|
||||
};
|
||||
|
||||
|
@ -99,11 +119,20 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
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;
|
||||
try {
|
||||
activeDevice.seekVideo(value.toDouble());
|
||||
} 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
|
||||
}
|
||||
|
||||
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;
|
||||
if (activeDevice.canSetVolume) {
|
||||
try {
|
||||
|
@ -121,6 +160,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
Logger.e(TAG, "Failed to change volume.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
@ -131,6 +171,35 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
super.show();
|
||||
Logger.i(TAG, "Dialog shown.");
|
||||
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onActiveDeviceVolumeChanged.remove(this)
|
||||
ExpStateCasting.instance.onActiveDeviceVolumeChanged.subscribe {
|
||||
_sliderVolume.value = it.toFloat().coerceAtLeast(0.0f).coerceAtMost(_sliderVolume.valueTo)
|
||||
}
|
||||
|
||||
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.subscribe {
|
||||
_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); };
|
||||
updateDevice();
|
||||
};
|
||||
}
|
||||
|
||||
updateDevice();
|
||||
}
|
||||
|
@ -167,9 +237,64 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||
_device = null;
|
||||
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() {
|
||||
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;
|
||||
|
||||
if (d is ChromecastCastingDevice) {
|
||||
|
@ -180,7 +305,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
_textType.text = "AirPlay";
|
||||
} else if (d is FCastCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
||||
_textType.text = "FastCast";
|
||||
_textType.text = "FCast";
|
||||
}
|
||||
|
||||
_textName.text = d.name;
|
||||
|
@ -220,6 +345,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableControls(views: List<View>) {
|
||||
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.ScriptUnavailableException
|
||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.fixHtmlLinks
|
||||
import com.futo.platformplayer.fixHtmlWhitespace
|
||||
import com.futo.platformplayer.getNowDiffSeconds
|
||||
|
@ -175,6 +176,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fcast.sender_sdk.DeviceFeature
|
||||
import userpackage.Protocol
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.Locale
|
||||
|
@ -664,6 +666,50 @@ class VideoDetailView : ConstraintLayout {
|
|||
}
|
||||
|
||||
if (!isInEditMode) {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState ->
|
||||
if (_onPauseCalled) {
|
||||
return@subscribe;
|
||||
}
|
||||
|
||||
when (connectionState) {
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> {
|
||||
loadCurrentVideo(lastPositionMilliseconds);
|
||||
updatePillButtonVisibilities();
|
||||
setCastEnabled(true);
|
||||
}
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> {
|
||||
loadCurrentVideo(lastPositionMilliseconds, playWhenReady = device.isPlaying);
|
||||
updatePillButtonVisibilities();
|
||||
setCastEnabled(false);
|
||||
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
ExpStateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
||||
val activeDevice = StateCasting.instance.activeDevice;
|
||||
if (activeDevice != null) {
|
||||
handlePlayChanged(it);
|
||||
|
||||
val v = video;
|
||||
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
|
||||
nextVideo();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ExpStateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) {
|
||||
if (_isCasting) {
|
||||
setLastPositionMilliseconds((it * 1000.0).toLong(), true);
|
||||
_cast.setTime(lastPositionMilliseconds);
|
||||
_timeBar.setPosition(it.toLong());
|
||||
_timeBar.setBufferedPosition(0);
|
||||
_timeBar.setDuration(video?.duration ?: 0);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { device, connectionState ->
|
||||
if (_onPauseCalled) {
|
||||
return@subscribe;
|
||||
|
@ -683,9 +729,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
}
|
||||
else -> {}
|
||||
}
|
||||
};
|
||||
|
||||
updatePillButtonVisibilities();
|
||||
}
|
||||
|
||||
StateCasting.instance.onActiveDevicePlayChanged.subscribe(this) {
|
||||
val activeDevice = StateCasting.instance.activeDevice;
|
||||
|
@ -708,6 +752,9 @@ class VideoDetailView : ConstraintLayout {
|
|||
_timeBar.setDuration(video?.duration ?: 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
updatePillButtonVisibilities();
|
||||
|
||||
_cast.onTimeJobTimeChanged_s.subscribe {
|
||||
if (_isCasting) {
|
||||
|
@ -1188,8 +1235,13 @@ class VideoDetailView : ConstraintLayout {
|
|||
_onPauseCalled = true;
|
||||
_taskLoadVideo.cancel();
|
||||
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
if(ExpStateCasting.instance.isCasting)
|
||||
return;
|
||||
} else {
|
||||
if(StateCasting.instance.isCasting)
|
||||
return;
|
||||
}
|
||||
|
||||
if(isAudioOnlyUserAction)
|
||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||
|
@ -1240,9 +1292,15 @@ class VideoDetailView : ConstraintLayout {
|
|||
_container_content_description.cleanup();
|
||||
_container_content_support.cleanup();
|
||||
StatePlayer.instance.autoplayChanged.remove(this)
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
||||
ExpStateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
} else {
|
||||
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
|
||||
}
|
||||
StateApp.instance.preventPictureInPicture.remove(this);
|
||||
StatePlayer.instance.onQueueChanged.remove(this);
|
||||
StatePlayer.instance.onVideoChanging.remove(this);
|
||||
|
@ -1974,7 +2032,11 @@ class VideoDetailView : ConstraintLayout {
|
|||
return;
|
||||
}
|
||||
|
||||
val isCasting = StateCasting.instance.isCasting
|
||||
val isCasting = if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.isCasting
|
||||
} else {
|
||||
StateCasting.instance.isCasting
|
||||
}
|
||||
if (!isCasting) {
|
||||
setCastEnabled(false);
|
||||
|
||||
|
@ -2051,11 +2113,19 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
|
||||
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)
|
||||
}, onLoadingEstimate = {
|
||||
_cast.setLoading(it)
|
||||
})
|
||||
} else {
|
||||
StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
|
||||
_cast.setLoading(it)
|
||||
}, onLoadingEstimate = {
|
||||
_cast.setLoading(it)
|
||||
})
|
||||
}
|
||||
|
||||
if (castingSucceeded) {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -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 {
|
||||
(it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString())
|
||||
};
|
||||
|
@ -2368,8 +2444,18 @@ class VideoDetailView : ConstraintLayout {
|
|||
?.distinct()
|
||||
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
||||
|
||||
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
||||
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
||||
val canSetSpeed = !_isCasting || if (Settings.instance.casting.experimentalCasting) {
|
||||
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;
|
||||
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
||||
R.string.quality), null, true,
|
||||
|
@ -2383,7 +2469,13 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
setButtons(playbackLabels, String.format(Locale.US, format, currentPlaybackRate));
|
||||
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;
|
||||
val stepSpeed = Settings.instance.playback.getPlaybackSpeedStep();
|
||||
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();
|
||||
val newPlaybackSpeed = playbackSpeedString.toDouble();
|
||||
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
|
||||
if (!ad.canSetSpeed) {
|
||||
return@subscribe
|
||||
|
@ -2400,6 +2502,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
||||
ad.changeSpeed(newPlaybackSpeed)
|
||||
setSelected(playbackSpeedString);
|
||||
}
|
||||
} else {
|
||||
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
|
||||
_player.setPlaybackRate(playbackSpeedString.toFloat());
|
||||
|
@ -2514,9 +2617,15 @@ class VideoDetailView : ConstraintLayout {
|
|||
//Handlers
|
||||
private fun handlePlay() {
|
||||
Logger.i(TAG, "handlePlay")
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
if (!ExpStateCasting.instance.resumeVideo()) {
|
||||
_player.play()
|
||||
}
|
||||
} else {
|
||||
if (!StateCasting.instance.resumeVideo()) {
|
||||
_player.play();
|
||||
}
|
||||
}
|
||||
onShouldEnterPictureInPictureChanged.emit()
|
||||
|
||||
//TODO: This was needed because handleLowerVolume was done.
|
||||
|
@ -2531,27 +2640,54 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
private fun handlePause() {
|
||||
Logger.i(TAG, "handlePause")
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
if (!ExpStateCasting.instance.pauseVideo()) {
|
||||
_player.pause()
|
||||
}
|
||||
} else {
|
||||
if (!StateCasting.instance.pauseVideo()) {
|
||||
_player.pause();
|
||||
_player.pause()
|
||||
}
|
||||
}
|
||||
onShouldEnterPictureInPictureChanged.emit()
|
||||
}
|
||||
private fun handleSeek(ms: Long) {
|
||||
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)) {
|
||||
_player.seekTo(ms);
|
||||
_player.seekTo(ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun handleStop() {
|
||||
Logger.i(TAG, "handleStop")
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
if (!ExpStateCasting.instance.stopVideo()) {
|
||||
_player.stop()
|
||||
}
|
||||
} else {
|
||||
if (!StateCasting.instance.stopVideo()) {
|
||||
_player.stop();
|
||||
_player.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePlayChanged(playing: Boolean) {
|
||||
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;
|
||||
if (ad != null) {
|
||||
_cast.setIsPlaying(playing);
|
||||
|
@ -2559,6 +2695,7 @@ class VideoDetailView : ConstraintLayout {
|
|||
StatePlayer.instance.updateMediaSession( null);
|
||||
StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, _player.exoPlayer?.player?.currentPosition ?: 0);
|
||||
}
|
||||
}
|
||||
|
||||
if(playing) {
|
||||
_minimize_controls_pause.visibility = View.VISIBLE;
|
||||
|
@ -2595,11 +2732,27 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
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;
|
||||
if (d != null && d.connectionState == 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?
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "handleSelectVideoTrack failed", e)
|
||||
}
|
||||
|
@ -2616,11 +2769,35 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
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)
|
||||
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,
|
||||
_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?
|
||||
} 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) {
|
||||
Logger.e(TAG, "handleSelectAudioTrack failed", e)
|
||||
}
|
||||
|
@ -2638,12 +2815,37 @@ class VideoDetailView : ConstraintLayout {
|
|||
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
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);
|
||||
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,
|
||||
_lastVideoSource,
|
||||
_lastAudioSource,
|
||||
toSet,
|
||||
(d.expectedCurrentTime * 1000.0).toLong(),
|
||||
d.speed
|
||||
);
|
||||
else {
|
||||
_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) {
|
||||
Logger.e(TAG, "handleSelectSubtitleTrack failed", e)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.futo.platformplayer.models
|
||||
|
||||
import com.futo.platformplayer.casting.CastProtocolType
|
||||
import com.futo.platformplayer.experimental_casting.ExpCastProtocolType
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class CastingDeviceInfo {
|
||||
|
@ -16,3 +17,18 @@ class CastingDeviceInfo {
|
|||
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.casting.CastingDevice
|
||||
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> {
|
||||
private val _devices: List<DeviceAdapterEntry>;
|
||||
|
||||
var onPin = Event1<CastingDevice>();
|
||||
var onConnect = Event1<CastingDevice>();
|
||||
var onPin = Event1<GenericCastingDevice>();
|
||||
var onConnect = Event1<GenericCastingDevice>();
|
||||
|
||||
constructor(devices: List<DeviceAdapterEntry>) : super() {
|
||||
_devices = devices;
|
||||
|
|
|
@ -4,21 +4,20 @@ import android.graphics.drawable.Animatable
|
|||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
||||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.CastingDevice
|
||||
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
||||
import com.futo.platformplayer.casting.FCastCastingDevice
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import androidx.core.view.isVisible
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import org.fcast.sender_sdk.ProtocolType
|
||||
|
||||
class DeviceViewHolder : ViewHolder {
|
||||
private val _layoutDevice: FrameLayout;
|
||||
|
@ -32,11 +31,11 @@ class DeviceViewHolder : ViewHolder {
|
|||
private var _animatableLoader: Animatable? = null;
|
||||
private var _imagePin: ImageView;
|
||||
|
||||
var device: CastingDevice? = null
|
||||
var device: GenericCastingDevice? = null
|
||||
private set
|
||||
|
||||
var onPin = Event1<CastingDevice>();
|
||||
val onConnect = Event1<CastingDevice>();
|
||||
var onPin = Event1<GenericCastingDevice>();
|
||||
val onConnect = Event1<GenericCastingDevice>();
|
||||
|
||||
constructor(view: View) : super(view) {
|
||||
_root = view.findViewById(R.id.layout_root);
|
||||
|
@ -56,9 +55,12 @@ class DeviceViewHolder : ViewHolder {
|
|||
|
||||
val connect = {
|
||||
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.connectDevice(dev)
|
||||
StateCasting.instance.connectDevice(dev.device)
|
||||
onConnect.emit(dev)
|
||||
} else {
|
||||
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() };
|
||||
|
@ -80,22 +98,26 @@ class DeviceViewHolder : ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
||||
if (d is ChromecastCastingDevice) {
|
||||
// fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
||||
|
||||
fun bind(d: GenericCastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
||||
when (d) {
|
||||
is GenericCastingDevice.Normal -> {
|
||||
if (d.device is ChromecastCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||
_textType.text = "Chromecast";
|
||||
} else if (d is AirPlayCastingDevice) {
|
||||
} else if (d.device is AirPlayCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||
_textType.text = "AirPlay";
|
||||
} else if (d is FCastCastingDevice) {
|
||||
} else if (d.device is FCastCastingDevice) {
|
||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
||||
_textType.text = "FCast";
|
||||
}
|
||||
|
||||
_textName.text = d.name;
|
||||
_imageOnline.visibility = if (isOnlineDevice && d.isReady) View.VISIBLE else View.GONE
|
||||
_textName.text = d.device.name;
|
||||
_imageOnline.visibility = if (isOnlineDevice && d.device.isReady) View.VISIBLE else View.GONE
|
||||
|
||||
if (!d.isReady) {
|
||||
if (!d.device.isReady) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.VISIBLE;
|
||||
_imagePin.visibility = View.GONE;
|
||||
|
@ -114,7 +136,7 @@ class DeviceViewHolder : ViewHolder {
|
|||
_imagePin.visibility = View.VISIBLE;
|
||||
}
|
||||
} else {
|
||||
if (d.isReady) {
|
||||
if (d.device.isReady) {
|
||||
_imageLoader.visibility = View.GONE;
|
||||
_textNotReady.visibility = View.GONE;
|
||||
_imagePin.visibility = View.VISIBLE;
|
||||
|
@ -136,4 +158,62 @@ class DeviceViewHolder : ViewHolder {
|
|||
|
||||
device = d;
|
||||
}
|
||||
is GenericCastingDevice.Experimental -> {
|
||||
when (d.handle.device.castingProtocol()) {
|
||||
ProtocolType.CHROMECAST -> {
|
||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||
_textType.text = "Chromecast";
|
||||
}
|
||||
ProtocolType.F_CAST -> {
|
||||
_imageDevice.setImageResource(R.drawable.ic_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;
|
||||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import com.futo.platformplayer.UIDialogs
|
|||
import com.futo.platformplayer.casting.CastConnectionState
|
||||
import com.futo.platformplayer.casting.StateCasting
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
|
||||
class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
||||
var onClick = Event1<Pair<String, Any>>();
|
||||
|
@ -27,9 +28,15 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
|||
visibility = View.GONE;
|
||||
}
|
||||
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
|
||||
updateCastState();
|
||||
};
|
||||
} else {
|
||||
StateCasting.instance.onActiveDeviceConnectionStateChanged.subscribe(this) { _, _ ->
|
||||
updateCastState();
|
||||
};
|
||||
}
|
||||
|
||||
updateCastState();
|
||||
}
|
||||
|
@ -37,6 +44,23 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
|||
|
||||
private fun updateCastState() {
|
||||
val c = context ?: return;
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
val d = ExpStateCasting.instance.activeDevice;
|
||||
|
||||
val activeColor = ContextCompat.getColor(c, R.color.colorPrimary);
|
||||
val connectingColor = ContextCompat.getColor(c, R.color.gray_c3);
|
||||
val inactiveColor = ContextCompat.getColor(c, R.color.white);
|
||||
|
||||
if (d != null) {
|
||||
when (d.connectionState) {
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTED -> setColorFilter(activeColor)
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.CONNECTING -> setColorFilter(connectingColor)
|
||||
com.futo.platformplayer.experimental_casting.CastConnectionState.DISCONNECTED -> setColorFilter(activeColor)
|
||||
}
|
||||
} else {
|
||||
setColorFilter(inactiveColor);
|
||||
}
|
||||
} else {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
|
||||
val activeColor = ContextCompat.getColor(c, R.color.colorPrimary);
|
||||
|
@ -53,9 +77,11 @@ class CastButton : androidx.appcompat.widget.AppCompatImageButton {
|
|||
setColorFilter(inactiveColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
setOnClickListener(null);
|
||||
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.Event1
|
||||
import com.futo.platformplayer.constructs.Event2
|
||||
import com.futo.platformplayer.experimental_casting.ExpStateCasting
|
||||
import com.futo.platformplayer.formatDuration
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
|
@ -38,6 +39,7 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.fcast.sender_sdk.DeviceFeature
|
||||
|
||||
class CastView : ConstraintLayout {
|
||||
private val _thumbnail: ImageView;
|
||||
|
@ -96,22 +98,48 @@ class CastView : ConstraintLayout {
|
|||
_gestureControlView.fullScreenGestureEnabled = false
|
||||
_gestureControlView.setupTouchArea();
|
||||
_gestureControlView.onSpeedHoldStart.subscribe {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
val d = ExpStateCasting.instance.activeDevice ?: return@subscribe;
|
||||
_speedHoldWasPlaying = d.isPlaying
|
||||
_speedHoldPrevRate = d.speed
|
||||
if (d.device.supportsFeature(DeviceFeature.SET_SPEED)) {
|
||||
d.device.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
||||
}
|
||||
d.device.resumePlayback()
|
||||
} else {
|
||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||
_speedHoldWasPlaying = d.isPlaying
|
||||
_speedHoldPrevRate = d.speed
|
||||
if (d.canSetSpeed)
|
||||
if (d.canSetSpeed) {
|
||||
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
||||
}
|
||||
d.resumeVideo()
|
||||
}
|
||||
}
|
||||
_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;
|
||||
if (!_speedHoldWasPlaying) d.pauseVideo()
|
||||
if (!_speedHoldWasPlaying) {
|
||||
d.pauseVideo()
|
||||
}
|
||||
d.changeSpeed(_speedHoldPrevRate)
|
||||
}
|
||||
}
|
||||
|
||||
_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;
|
||||
StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
|
||||
}
|
||||
};
|
||||
|
||||
_buttonLoop.setOnClickListener {
|
||||
|
@ -122,22 +150,46 @@ class CastView : ConstraintLayout {
|
|||
|
||||
_timeBar.addListener(object : TimeBar.OnScrubListener {
|
||||
override fun onScrubStart(timeBar: TimeBar, position: Long) {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.videoSeekTo(position.toDouble());
|
||||
} else {
|
||||
StateCasting.instance.videoSeekTo(position.toDouble());
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
||||
if (Settings.instance.casting.experimentalCasting) {
|
||||
ExpStateCasting.instance.videoSeekTo(position.toDouble());
|
||||
} else {
|
||||
StateCasting.instance.videoSeekTo(position.toDouble());
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_buttonMinimize.setOnClickListener { onMinimizeClick.emit(); };
|
||||
_buttonSettings.setOnClickListener { onSettingsClick.emit(); };
|
||||
_buttonPlay.setOnClickListener { StateCasting.instance.resumeVideo(); };
|
||||
_buttonPause.setOnClickListener { StateCasting.instance.pauseVideo(); };
|
||||
_buttonPlay.setOnClickListener {
|
||||
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) {
|
||||
setIsPlaying(false);
|
||||
|
@ -220,6 +272,8 @@ class CastView : ConstraintLayout {
|
|||
stopTimeJob()
|
||||
|
||||
if(isPlaying) {
|
||||
// NOTE: the experimental implementation polls automatically
|
||||
if (!Settings.instance.casting.experimentalCasting) {
|
||||
val d = StateCasting.instance.activeDevice;
|
||||
if (d is AirPlayCastingDevice || d is ChromecastCastingDevice) {
|
||||
_updateTimeJob = _scope.launch {
|
||||
|
@ -236,6 +290,7 @@ class CastView : ConstraintLayout {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_inPictureInPicture) {
|
||||
_buttonPause.visibility = View.VISIBLE;
|
||||
|
@ -247,7 +302,11 @@ class CastView : ConstraintLayout {
|
|||
_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()) {
|
||||
StatePlayer.instance.updateMediaSession(null);
|
||||
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), (position ?: 0));
|
||||
|
@ -311,6 +370,14 @@ class CastView : ConstraintLayout {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
return when(d.isPlaying) {
|
||||
|
@ -318,6 +385,7 @@ class CastView : ConstraintLayout {
|
|||
else -> PlaybackStateCompat.STATE_PAUSED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setLoading(isLoading: Boolean) {
|
||||
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_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="experimental_cast">Experimental</string>
|
||||
<string name="experimental_cast_description">Use experimental casting backend (requires restart)</string>
|
||||
<string name="discover">Discover</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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue