Merge branch 'marcus/casting-sdk-updates' into 'master'

casting: subscribe to and handle MediaItemEnd events

See merge request videostreaming/grayjay!158
This commit is contained in:
Koen
2025-12-15 09:01:31 +00:00
6 changed files with 46 additions and 13 deletions
+1 -1
View File
@@ -232,7 +232,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
//Rust casting SDK //Rust casting SDK
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') { implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.4.0') {
// Polycentricandroid includes this // Polycentricandroid includes this
exclude group: 'net.java.dev.jna' exclude group: 'net.java.dev.jna'
} }
@@ -1,5 +1,6 @@
package com.futo.platformplayer.casting package com.futo.platformplayer.casting
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
import org.fcast.sender_sdk.Metadata import org.fcast.sender_sdk.Metadata
@@ -16,6 +17,7 @@ abstract class CastingDevice {
abstract val onDurationChanged: Event1<Double> abstract val onDurationChanged: Event1<Double>
abstract val onVolumeChanged: Event1<Double> abstract val onVolumeChanged: Event1<Double>
abstract val onSpeedChanged: Event1<Double> abstract val onSpeedChanged: Event1<Double>
abstract val onMediaItemEnd: Event0
abstract var connectionState: CastConnectionState abstract var connectionState: CastConnectionState
abstract val protocolType: CastProtocolType abstract val protocolType: CastProtocolType
abstract var isPlaying: Boolean abstract var isPlaying: Boolean
@@ -2,12 +2,14 @@ package com.futo.platformplayer.casting
import android.os.Build import android.os.Build
import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.polycentric.core.Event
import org.fcast.sender_sdk.ApplicationInfo import org.fcast.sender_sdk.ApplicationInfo
import org.fcast.sender_sdk.GenericKeyEvent import org.fcast.sender_sdk.KeyEvent
import org.fcast.sender_sdk.GenericMediaEvent import org.fcast.sender_sdk.MediaEvent
import org.fcast.sender_sdk.PlaybackState import org.fcast.sender_sdk.PlaybackState
import org.fcast.sender_sdk.Source import org.fcast.sender_sdk.Source
import java.net.InetAddress import java.net.InetAddress
@@ -15,8 +17,10 @@ import org.fcast.sender_sdk.CastingDevice as RsCastingDevice;
import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler; import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler;
import org.fcast.sender_sdk.DeviceConnectionState import org.fcast.sender_sdk.DeviceConnectionState
import org.fcast.sender_sdk.DeviceFeature import org.fcast.sender_sdk.DeviceFeature
import org.fcast.sender_sdk.EventSubscription
import org.fcast.sender_sdk.IpAddr import org.fcast.sender_sdk.IpAddr
import org.fcast.sender_sdk.LoadRequest import org.fcast.sender_sdk.LoadRequest
import org.fcast.sender_sdk.MediaItemEventType
import org.fcast.sender_sdk.Metadata import org.fcast.sender_sdk.Metadata
import org.fcast.sender_sdk.ProtocolType import org.fcast.sender_sdk.ProtocolType
import org.fcast.sender_sdk.urlFormatIpAddr import org.fcast.sender_sdk.urlFormatIpAddr
@@ -63,6 +67,7 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
var onDurationChanged = Event1<Double>() var onDurationChanged = Event1<Double>()
var onVolumeChanged = Event1<Double>() var onVolumeChanged = Event1<Double>()
var onSpeedChanged = Event1<Double>() var onSpeedChanged = Event1<Double>()
var onMediaItemEnd = Event0()
override fun connectionStateChanged(state: DeviceConnectionState) { override fun connectionStateChanged(state: DeviceConnectionState) {
onConnectionStateChanged.emit(state) onConnectionStateChanged.emit(state)
@@ -92,12 +97,14 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
// TODO // TODO
} }
override fun keyEvent(event: GenericKeyEvent) { override fun keyEvent(event: KeyEvent) {
// Unreachable // Unreachable
} }
override fun mediaEvent(event: GenericMediaEvent) { override fun mediaEvent(event: MediaEvent) {
// Unreachable if (event.type == MediaItemEventType.END) {
onMediaItemEnd.emit()
}
} }
override fun playbackError(message: String) { override fun playbackError(message: String) {
@@ -127,6 +134,8 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
get() = eventHandler.onVolumeChanged get() = eventHandler.onVolumeChanged
override val onSpeedChanged: Event1<Double> override val onSpeedChanged: Event1<Double>
get() = eventHandler.onSpeedChanged get() = eventHandler.onSpeedChanged
override val onMediaItemEnd: Event0
get() = eventHandler.onMediaItemEnd
override fun resumePlayback() = device.resumePlayback() override fun resumePlayback() = device.resumePlayback()
override fun pausePlayback() = device.pausePlayback() override fun pausePlayback() = device.pausePlayback()
@@ -181,7 +190,8 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
resumePosition = resumePosition, resumePosition = resumePosition,
speed = speed, speed = speed,
volume = volume, volume = volume,
metadata = metadata metadata = metadata,
requestHeaders = null,
) )
) )
@@ -200,6 +210,7 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
speed = speed, speed = speed,
volume = volume, volume = volume,
metadata = metadata, metadata = metadata,
requestHeaders = null,
) )
) )
@@ -227,6 +238,13 @@ class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
eventHandler.onConnectionStateChanged.subscribe { newState -> eventHandler.onConnectionStateChanged.subscribe { newState ->
when (newState) { when (newState) {
is DeviceConnectionState.Connected -> { is DeviceConnectionState.Connected -> {
if (device.supportsFeature(DeviceFeature.MEDIA_EVENT_SUBSCRIPTION)) {
try {
device.subscribeEvent(EventSubscription.MediaItemEnd)
} catch (e: Exception) {
Logger.e(TAG, "Failed to subscribe to MediaItemEnd events: $e")
}
}
usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr) usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr)
localAddress = ipAddrToInetAddress(newState.localAddr) localAddress = ipAddrToInetAddress(newState.localAddr)
connectionState = CastConnectionState.CONNECTED connectionState = CastConnectionState.CONNECTED
@@ -1,5 +1,6 @@
package com.futo.platformplayer.casting package com.futo.platformplayer.casting
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
@@ -181,6 +182,7 @@ class CastingDeviceLegacyWrapper(val inner: CastingDeviceLegacy) : CastingDevice
override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged
override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged
override val onSpeedChanged: Event1<Double> get() = inner.onSpeedChanged override val onSpeedChanged: Event1<Double> get() = inner.onSpeedChanged
override val onMediaItemEnd: Event0 = Event0()
override var connectionState: CastConnectionState override var connectionState: CastConnectionState
get() = inner.connectionState get() = inner.connectionState
set(_) = Unit set(_) = Unit
@@ -40,6 +40,7 @@ import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAud
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
import com.futo.platformplayer.awaitCancelConverted import com.futo.platformplayer.awaitCancelConverted
import com.futo.platformplayer.builders.DashBuilder import com.futo.platformplayer.builders.DashBuilder
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.models.CastingDeviceInfo import com.futo.platformplayer.models.CastingDeviceInfo
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
@@ -82,6 +83,7 @@ abstract class StateCasting {
val onActiveDeviceTimeChanged = Event1<Double>(); val onActiveDeviceTimeChanged = Event1<Double>();
val onActiveDeviceDurationChanged = Event1<Double>(); val onActiveDeviceDurationChanged = Event1<Double>();
val onActiveDeviceVolumeChanged = Event1<Double>(); val onActiveDeviceVolumeChanged = Event1<Double>();
val onActiveDeviceMediaItemEnd = Event0()
var activeDevice: CastingDevice? = null; var activeDevice: CastingDevice? = null;
private var _videoExecutor: JSRequestExecutor? = null private var _videoExecutor: JSRequestExecutor? = null
private var _audioExecutor: JSRequestExecutor? = null private var _audioExecutor: JSRequestExecutor? = null
@@ -145,6 +147,7 @@ abstract class StateCasting {
device.onTimeChanged.clear(); device.onTimeChanged.clear();
device.onVolumeChanged.clear(); device.onVolumeChanged.clear();
device.onDurationChanged.clear(); device.onDurationChanged.clear();
device.onMediaItemEnd.clear();
ad.disconnect() ad.disconnect()
} }
@@ -159,6 +162,7 @@ abstract class StateCasting {
device.onTimeChanged.clear(); device.onTimeChanged.clear();
device.onVolumeChanged.clear(); device.onVolumeChanged.clear();
device.onDurationChanged.clear(); device.onDurationChanged.clear();
device.onMediaItemEnd.clear();
activeDevice = null; activeDevice = null;
} }
@@ -222,6 +226,9 @@ abstract class StateCasting {
device.onTimeChanged.subscribe { device.onTimeChanged.subscribe {
invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) }; invokeInMainScopeIfRequired { onActiveDeviceTimeChanged.emit(it) };
}; };
device.onMediaItemEnd.subscribe {
invokeInMainScopeIfRequired { onActiveDeviceMediaItemEnd.emit() }
}
try { try {
device.connect(); device.connect();
@@ -232,6 +239,7 @@ abstract class StateCasting {
device.onTimeChanged.clear(); device.onTimeChanged.clear();
device.onVolumeChanged.clear(); device.onVolumeChanged.clear();
device.onDurationChanged.clear(); device.onDurationChanged.clear();
device.onMediaItemEnd.clear();
return; return;
} }
@@ -723,15 +723,17 @@ class VideoDetailView : ConstraintLayout {
val activeDevice = StateCasting.instance.activeDevice; val activeDevice = StateCasting.instance.activeDevice;
if (activeDevice != null) { if (activeDevice != null) {
handlePlayChanged(it); handlePlayChanged(it);
val v = video;
if (!it && v != null && v.duration - activeDevice.time.toLong() < 2L) {
Log.i(TAG, "Next video (loop?)")
nextVideo();
}
} }
}; };
StateCasting.instance.onActiveDeviceMediaItemEnd.subscribe(this) {
val activeDevice = StateCasting.instance.activeDevice;
if (activeDevice != null) {
Log.i(TAG, "Next video (loop?)")
nextVideo();
}
}
StateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) { StateCasting.instance.onActiveDeviceTimeChanged.subscribe(this) {
if (_isCasting) { if (_isCasting) {
setLastPositionMilliseconds((it * 1000.0).toLong(), true); setLastPositionMilliseconds((it * 1000.0).toLong(), true);
@@ -1273,6 +1275,7 @@ class VideoDetailView : ConstraintLayout {
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);
StateCasting.instance.onActiveDeviceMediaItemEnd.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);