casting: log exceptions from playback controls

This commit is contained in:
Marcus Hanestad 2025-09-04 13:08:40 +02:00
commit 3de5cd92ae
6 changed files with 94 additions and 103 deletions

View file

@ -128,33 +128,15 @@ class ExpCastingDevice(val device: RsCastingDevice) : CastingDevice() {
override val onSpeedChanged: Event1<Double> override val onSpeedChanged: Event1<Double>
get() = eventHandler.onSpeedChanged get() = eventHandler.onSpeedChanged
override fun resumePlayback() = try { override fun resumePlayback() = device.resumePlayback()
device.resumePlayback() override fun pausePlayback() = device.pausePlayback()
} catch (_: Throwable) { override fun stopPlayback() = device.stopPlayback()
} override fun seekTo(timeSeconds: Double) = device.seek(timeSeconds)
override fun pausePlayback() = try {
device.pausePlayback()
} catch (_: Throwable) {
}
override fun stopPlayback() = try {
device.stopPlayback()
} catch (_: Throwable) {
}
override fun seekTo(timeSeconds: Double) = try {
device.seek(timeSeconds)
} catch (_: Throwable) {
}
override fun changeVolume(newVolume: Double) { override fun changeVolume(newVolume: Double) {
device.changeVolume(newVolume) device.changeVolume(newVolume)
volume = newVolume volume = newVolume
} }
override fun changeSpeed(speed: Double) = device.changeSpeed(speed) override fun changeSpeed(speed: Double) = device.changeSpeed(speed)
override fun connect() = device.connect( override fun connect() = device.connect(
ApplicationInfo( ApplicationInfo(
"Grayjay Android", "Grayjay Android",
@ -192,19 +174,16 @@ class ExpCastingDevice(val device: RsCastingDevice) : CastingDevice() {
duration: Double, duration: Double,
speed: Double?, speed: Double?,
metadata: Metadata? metadata: Metadata?
) = try { ) = device.load(
device.load( LoadRequest.Video(
LoadRequest.Video( contentType = contentType,
contentType = contentType, url = contentId,
url = contentId, resumePosition = resumePosition,
resumePosition = resumePosition, speed = speed,
speed = speed, volume = volume,
volume = volume, metadata = metadata
metadata = metadata
)
) )
} catch (_: Throwable) { )
}
override fun loadContent( override fun loadContent(
contentType: String, contentType: String,
@ -213,19 +192,16 @@ class ExpCastingDevice(val device: RsCastingDevice) : CastingDevice() {
duration: Double, duration: Double,
speed: Double?, speed: Double?,
metadata: Metadata? metadata: Metadata?
) = try { ) = device.load(
device.load( LoadRequest.Content(
LoadRequest.Content( contentType = contentType,
contentType = contentType, content = content,
content = content, resumePosition = resumePosition,
resumePosition = resumePosition, speed = speed,
speed = speed, volume = volume,
volume = volume, metadata = metadata,
metadata = metadata,
)
) )
} catch (_: Throwable) { )
}
override var connectionState = CastConnectionState.DISCONNECTED override var connectionState = CastConnectionState.DISCONNECTED
override val protocolType: CastProtocolType override val protocolType: CastProtocolType

View file

@ -45,6 +45,8 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.stores.CastingDeviceInfoStorage import com.futo.platformplayer.stores.CastingDeviceInfoStorage
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.toUrlAddress import com.futo.platformplayer.toUrlAddress
import com.futo.platformplayer.views.casting.CastView
import com.futo.platformplayer.views.casting.CastView.Companion
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -238,6 +240,7 @@ abstract class StateCasting {
) )
} }
@Throws
suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null): Boolean { suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, ms: Long = -1, speed: Double?, onLoadingEstimate: ((Int) -> Unit)? = null, onLoading: ((Boolean) -> Unit)? = null): Boolean {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val ad = activeDevice ?: return@withContext false; val ad = activeDevice ?: return@withContext false;
@ -348,7 +351,9 @@ abstract class StateCasting {
val ad = activeDevice ?: return false; val ad = activeDevice ?: return false;
try { try {
ad.resumePlayback(); ad.resumePlayback();
} catch (_: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to resume playback: $e")
return false
} }
return true; return true;
} }
@ -357,7 +362,9 @@ abstract class StateCasting {
val ad = activeDevice ?: return false; val ad = activeDevice ?: return false;
try { try {
ad.pausePlayback(); ad.pausePlayback();
} catch (_: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to pause playback: $e")
return false
} }
return true; return true;
} }
@ -366,7 +373,9 @@ abstract class StateCasting {
val ad = activeDevice ?: return false; val ad = activeDevice ?: return false;
try { try {
ad.stopPlayback(); ad.stopPlayback();
} catch (_: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to stop playback: $e")
return false
} }
return true; return true;
} }
@ -375,11 +384,34 @@ abstract class StateCasting {
val ad = activeDevice ?: return false; val ad = activeDevice ?: return false;
try { try {
ad.seekTo(timeSeconds); ad.seekTo(timeSeconds);
} catch (_: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to seek: $e")
return false
} }
return true; return true;
} }
fun changeVolume(volume: Double): Boolean {
val ad = activeDevice ?: return false;
try {
ad.changeVolume(volume);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to change volume: $e")
return false
}
return true;
}
fun changeSpeed(speed: Double): Boolean {
val ad = activeDevice ?: return false;
try {
ad.changeSpeed(speed);
} catch (e: Throwable) {
Logger.e(TAG, "Failed to change speed: $e")
return false
}
return true;
}
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> { private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
val ad = activeDevice ?: return listOf(); val ad = activeDevice ?: return listOf();

View file

@ -74,24 +74,18 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_buttonPlay = findViewById(R.id.button_play); _buttonPlay = findViewById(R.id.button_play);
_buttonPlay.setOnClickListener { _buttonPlay.setOnClickListener {
try { StateCasting.instance.resumeVideo()
StateCasting.instance.activeDevice?.resumePlayback()
} catch (_: Throwable) {}
} }
_buttonPause = findViewById(R.id.button_pause); _buttonPause = findViewById(R.id.button_pause);
_buttonPause.setOnClickListener { _buttonPause.setOnClickListener {
try { StateCasting.instance.pauseVideo()
StateCasting.instance.activeDevice?.pausePlayback()
} catch (_: Throwable) {}
} }
_buttonStop = findViewById(R.id.button_stop); _buttonStop = findViewById(R.id.button_stop);
_buttonStop.setOnClickListener { _buttonStop.setOnClickListener {
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails() (ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
try { StateCasting.instance.stopVideo()
StateCasting.instance.activeDevice?.stopPlayback()
} catch (_: Throwable) {}
} }
_buttonNext = findViewById(R.id.button_next); _buttonNext = findViewById(R.id.button_next);
@ -103,7 +97,9 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
_buttonDisconnect.setOnClickListener { _buttonDisconnect.setOnClickListener {
try { try {
StateCasting.instance.activeDevice?.disconnect() StateCasting.instance.activeDevice?.disconnect()
} catch (_: Throwable) {} } catch (e: Throwable) {
Logger.e(TAG, "Active device failed to disconnect: $e")
}
dismiss(); dismiss();
}; };
@ -112,9 +108,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
return@OnChangeListener return@OnChangeListener
} }
try { StateCasting.instance.videoSeekTo(value.toDouble())
StateCasting.instance.activeDevice?.seekTo(value.toDouble())
} catch (_: Throwable) {}
}); });
//TODO: Check if volume slider is properly hidden in all cases //TODO: Check if volume slider is properly hidden in all cases
@ -123,9 +117,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
return@OnChangeListener return@OnChangeListener
} }
try { StateCasting.instance.changeVolume(value.toDouble())
StateCasting.instance.activeDevice?.changeVolume(value.toDouble())
} catch (_: Throwable) {}
}); });
setLoading(false); setLoading(false);

View file

@ -577,10 +577,7 @@ class VideoDetailView : ConstraintLayout {
if(chapter?.type == ChapterType.SKIPPABLE) { if(chapter?.type == ChapterType.SKIPPABLE) {
_layoutSkip.visibility = VISIBLE; _layoutSkip.visibility = VISIBLE;
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) { } else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
val ad = StateCasting.instance.activeDevice if (!StateCasting.instance.videoSeekTo(chapter.timeEnd)) {
if (ad != null) {
ad.seekTo(chapter.timeEnd)
} else {
_player.seekTo((chapter.timeEnd * 1000).toLong()); _player.seekTo((chapter.timeEnd * 1000).toLong());
} }
@ -2383,9 +2380,7 @@ class VideoDetailView : ConstraintLayout {
val newPlaybackSpeed = playbackSpeedString.toDouble(); val newPlaybackSpeed = playbackSpeedString.toDouble();
if (_isCasting && StateCasting.instance.activeDevice?.canSetSpeed() ?: false) { if (_isCasting && StateCasting.instance.activeDevice?.canSetSpeed() ?: false) {
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})"); qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");
try { StateCasting.instance.changeSpeed(newPlaybackSpeed)
StateCasting.instance.activeDevice?.changeSpeed(newPlaybackSpeed)
} catch (_: Throwable) {}
setSelected(playbackSpeedString); setSelected(playbackSpeedString);
} else { } else {
qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})"); qualityPlaybackSpeedTitle?.setTitle(context.getString(R.string.playback_rate) + " (${String.format(Locale.US, "%.2f", newPlaybackSpeed)})");

View file

@ -16,6 +16,7 @@ import com.futo.platformplayer.casting.CastProtocolType
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.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
class DeviceViewHolder : ViewHolder { class DeviceViewHolder : ViewHolder {
private val _layoutDevice: FrameLayout; private val _layoutDevice: FrameLayout;
@ -63,7 +64,9 @@ class DeviceViewHolder : ViewHolder {
UIDialogs.toast(it, "Device not ready, may be offline") UIDialogs.toast(it, "Device not ready, may be offline")
} }
} }
} catch (_: Throwable) { } } catch (e: Throwable) {
Logger.e(TAG, "Failed to connect: $e")
}
} }
} }
@ -142,4 +145,8 @@ class DeviceViewHolder : ViewHolder {
device = d; device = d;
} }
companion object {
private val TAG = "DeviceViewHolder"
}
} }

View file

@ -27,6 +27,7 @@ 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.formatDuration import com.futo.platformplayer.formatDuration
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.TargetTapLoaderView import com.futo.platformplayer.views.TargetTapLoaderView
import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.behavior.GestureControlView
@ -96,17 +97,13 @@ class CastView : ConstraintLayout {
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()) {
try {
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
} catch (e: Throwable) {
// Ignored
}
}
try { try {
if (d.canSetSpeed()) {
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
}
d.resumePlayback() d.resumePlayback()
} catch (e: Throwable) { } catch (e: Throwable) {
// Ignored Logger.e(TAG, "$e")
} }
} }
_gestureControlView.onSpeedHoldEnd.subscribe { _gestureControlView.onSpeedHoldEnd.subscribe {
@ -117,16 +114,14 @@ class CastView : ConstraintLayout {
} }
d.changeSpeed(_speedHoldPrevRate) d.changeSpeed(_speedHoldPrevRate)
} catch (e: Throwable) { } catch (e: Throwable) {
// Ignored Logger.e(TAG, "$e")
} }
} }
_gestureControlView.onSeek.subscribe { _gestureControlView.onSeek.subscribe {
try { val d = StateCasting.instance.activeDevice ?: return@subscribe
val d = StateCasting.instance.activeDevice ?: return@subscribe val expectedCurrentTime = d.expectedCurrentTime
val expectedCurrentTime = d.expectedCurrentTime StateCasting.instance.videoSeekTo(expectedCurrentTime + it / 1000)
d.seekTo(expectedCurrentTime + it / 1000)
} catch (_: Throwable) { }
}; };
_buttonLoop.setOnClickListener { _buttonLoop.setOnClickListener {
@ -137,35 +132,25 @@ 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) {
try { StateCasting.instance.videoSeekTo(position.toDouble())
StateCasting.instance.activeDevice?.seekTo(position.toDouble())
} catch (_: Throwable) { }
} }
override fun onScrubMove(timeBar: TimeBar, position: Long) { override fun onScrubMove(timeBar: TimeBar, position: Long) {
try { StateCasting.instance.videoSeekTo(position.toDouble())
StateCasting.instance.activeDevice?.seekTo(position.toDouble())
} catch (_: Throwable) { }
} }
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
try { StateCasting.instance.videoSeekTo(position.toDouble())
StateCasting.instance.activeDevice?.seekTo(position.toDouble())
} catch (_: Throwable) { }
} }
}); });
_buttonMinimize.setOnClickListener { onMinimizeClick.emit(); }; _buttonMinimize.setOnClickListener { onMinimizeClick.emit(); };
_buttonSettings.setOnClickListener { onSettingsClick.emit(); }; _buttonSettings.setOnClickListener { onSettingsClick.emit(); };
_buttonPlay.setOnClickListener { _buttonPlay.setOnClickListener {
try { StateCasting.instance.resumeVideo()
StateCasting.instance.activeDevice?.resumePlayback()
} catch (_: Throwable) { }
} }
_buttonPause.setOnClickListener { _buttonPause.setOnClickListener {
try { StateCasting.instance.pauseVideo()
StateCasting.instance.activeDevice?.pausePlayback()
} catch (_: Throwable) { }
} }
if (!isInEditMode) { if (!isInEditMode) {
@ -350,4 +335,8 @@ class CastView : ConstraintLayout {
_loaderGame.visibility = View.VISIBLE _loaderGame.visibility = View.VISIBLE
_loaderGame.startLoader(expectedDurationMs.toLong()) _loaderGame.startLoader(expectedDurationMs.toLong())
} }
companion object {
private val TAG = "CastView";
}
} }