mirror of
https://github.com/jellyfin/jellyfin-androidtv.git
synced 2026-10-26 16:45:23 +00:00
Add timed event API
This commit is contained in:
1 parent
333c8b9630
commit
6a03b75eb2
8 files changed
+136
No files matched your search
@@ -9,6 +9,7 @@ import org.jellyfin.playback.core.mediastream.MediaStreamService
|
||||
import org.jellyfin.playback.core.plugin.PlaybackPlugin
|
||||
import org.jellyfin.playback.core.plugin.PlayerService
|
||||
import org.jellyfin.playback.core.queue.QueueService
|
||||
import org.jellyfin.playback.core.timedevent.TimedEventService
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -50,6 +51,7 @@ class PlaybackManagerBuilder(context: Context) {
|
||||
// Add default services
|
||||
services.add(QueueService())
|
||||
services.add(MediaStreamService(mediaStreamResolvers))
|
||||
services.add(TimedEventService())
|
||||
|
||||
// Only support a single backend right now
|
||||
require(backends.size == 1)
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.jellyfin.playback.core.mediastream.MediaStream
|
||||
import org.jellyfin.playback.core.model.PositionInfo
|
||||
import org.jellyfin.playback.core.queue.QueueEntry
|
||||
import org.jellyfin.playback.core.support.PlaySupportReport
|
||||
import org.jellyfin.playback.core.timedevent.TimedEvent
|
||||
import org.jellyfin.playback.core.ui.PlayerSubtitleView
|
||||
import org.jellyfin.playback.core.ui.PlayerSurfaceView
|
||||
import kotlin.time.Duration
|
||||
@@ -38,5 +39,7 @@ interface PlayerBackend {
|
||||
fun setScrubbing(scrubbing: Boolean)
|
||||
|
||||
fun setSpeed(speed: Float)
|
||||
|
||||
fun setTimedEvents(timedEvents: Collection<TimedEvent>)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.jellyfin.playback.core.timedevent
|
||||
|
||||
import kotlin.time.Duration
|
||||
|
||||
sealed interface TimedEvent {
|
||||
/**
|
||||
* Optional key for identifying this timed event.
|
||||
*/
|
||||
val key: String?
|
||||
|
||||
/**
|
||||
* The position of this timed event.
|
||||
*/
|
||||
val position: Duration
|
||||
|
||||
/**
|
||||
* Function callback variant of a timed event.
|
||||
*/
|
||||
data class Callback(
|
||||
override val key: String? = null,
|
||||
override val position: Duration,
|
||||
|
||||
val callback: () -> Unit,
|
||||
) : TimedEvent
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.jellyfin.playback.core.timedevent
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.plus
|
||||
import org.jellyfin.playback.core.plugin.PlayerService
|
||||
import org.jellyfin.playback.core.queue.queue
|
||||
import timber.log.Timber
|
||||
|
||||
class TimedEventService : PlayerService() {
|
||||
override suspend fun onInitialize() {
|
||||
manager.queue.entry
|
||||
.filterNotNull()
|
||||
.flatMapLatest { entry -> entry.timedEventsFlow }
|
||||
.onEach { timedEvents ->
|
||||
Timber.d("Timed events changed to $timedEvents")
|
||||
manager.backend.setTimedEvents(timedEvents.orEmpty())
|
||||
}.launchIn(coroutineScope + Dispatchers.Main)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.jellyfin.playback.core.timedevent
|
||||
|
||||
import org.jellyfin.playback.core.element.ElementKey
|
||||
import org.jellyfin.playback.core.element.element
|
||||
import org.jellyfin.playback.core.element.elementFlow
|
||||
import org.jellyfin.playback.core.queue.QueueEntry
|
||||
|
||||
private val timedEventsKey = ElementKey<Collection<TimedEvent>>("TimedEvents")
|
||||
|
||||
/**
|
||||
* Get or set the [Collection<TimedEvent>] for this [QueueEntry].
|
||||
*/
|
||||
var QueueEntry.timedEvents by element(timedEventsKey)
|
||||
val QueueEntry.timedEventsFlow by elementFlow(timedEventsKey)
|
||||
|
||||
fun QueueEntry.addTimedEvent(timedEvent: TimedEvent) {
|
||||
val timedEvents = getOrNull(timedEventsKey).orEmpty()
|
||||
val newTimedEvents = timedEvents + timedEvent
|
||||
put(timedEventsKey, newTimedEvents)
|
||||
}
|
||||
|
||||
fun QueueEntry.removeTimedEvent(timedEvent: TimedEvent) {
|
||||
val timedEvents = getOrNull(timedEventsKey).orEmpty()
|
||||
val newTimedEvents = timedEvents - timedEvent
|
||||
if (newTimedEvents.isEmpty()) remove(timedEventsKey)
|
||||
else put(timedEventsKey, newTimedEvents)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.jellyfin.playback.jellyfin
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import org.jellyfin.playback.core.plugin.playbackPlugin
|
||||
import org.jellyfin.playback.jellyfin.lyrics.LyricsPlayerService
|
||||
import org.jellyfin.playback.jellyfin.mediasegment.MediaSegmentService
|
||||
import org.jellyfin.playback.jellyfin.mediastream.JellyfinMediaStreamResolver
|
||||
import org.jellyfin.playback.jellyfin.playsession.PlaySessionService
|
||||
import org.jellyfin.playback.jellyfin.playsession.PlaySessionSocketService
|
||||
@@ -21,4 +22,5 @@ fun jellyfinPlugin(
|
||||
provide(PlaySessionSocketService(api, playSessionService, lifecycle))
|
||||
|
||||
provide(LyricsPlayerService(api))
|
||||
provide(MediaSegmentService(api))
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.PlayerMessage
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import androidx.media3.exoplayer.util.EventLogger
|
||||
@@ -33,6 +34,7 @@ import org.jellyfin.playback.core.model.PlayState
|
||||
import org.jellyfin.playback.core.model.PositionInfo
|
||||
import org.jellyfin.playback.core.queue.QueueEntry
|
||||
import org.jellyfin.playback.core.support.PlaySupportReport
|
||||
import org.jellyfin.playback.core.timedevent.TimedEvent
|
||||
import org.jellyfin.playback.core.ui.PlayerSubtitleView
|
||||
import org.jellyfin.playback.core.ui.PlayerSurfaceView
|
||||
import org.jellyfin.playback.media3.exoplayer.support.getPlaySupportReport
|
||||
@@ -56,6 +58,7 @@ class ExoPlayerBackend(
|
||||
private var subtitleView: SubtitleView? = null
|
||||
private val audioPipeline = ExoPlayerAudioPipeline()
|
||||
private val audioAttributeState = AudioAttributeState()
|
||||
private val messageHandler = ExoPlayerMessageHandler()
|
||||
|
||||
private val exoPlayer by lazy {
|
||||
val dataSourceFactory = DefaultDataSource.Factory(
|
||||
@@ -278,4 +281,26 @@ class ExoPlayerBackend(
|
||||
buffer = exoPlayer.bufferedPosition.milliseconds,
|
||||
duration = if (exoPlayer.duration == C.TIME_UNSET) Duration.ZERO else exoPlayer.duration.milliseconds,
|
||||
)
|
||||
|
||||
private var _currentTimedEventMessages = listOf<PlayerMessage>()
|
||||
override fun setTimedEvents(timedEvents: Collection<TimedEvent>) {
|
||||
// Cancel any existing timed messages
|
||||
for (message in _currentTimedEventMessages) {
|
||||
message.cancel()
|
||||
}
|
||||
|
||||
// Create new timed messages
|
||||
_currentTimedEventMessages = timedEvents.map { timedEvent ->
|
||||
val positionMs = timedEvent.position.inWholeMilliseconds
|
||||
// Messages at position 0 will never be invoked by ExoPlayer
|
||||
.coerceAtLeast(1L)
|
||||
|
||||
exoPlayer.createMessage(messageHandler).apply {
|
||||
setType(ExoPlayerMessageHandler.TYPE_TIMED_EVENT)
|
||||
setPosition(positionMs)
|
||||
setPayload(timedEvent)
|
||||
setDeleteAfterDelivery(false)
|
||||
}.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.jellyfin.playback.media3.exoplayer
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.PlayerMessage
|
||||
import org.jellyfin.playback.core.timedevent.TimedEvent
|
||||
import timber.log.Timber
|
||||
|
||||
class ExoPlayerMessageHandler : PlayerMessage.Target {
|
||||
companion object {
|
||||
const val TYPE_TIMED_EVENT = 1
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun handleMessage(messageType: Int, message: Any?): Unit = when (messageType) {
|
||||
TYPE_TIMED_EVENT -> {
|
||||
val timedEvent = message as? TimedEvent ?: return
|
||||
handleTimedEventMessage(timedEvent)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.w("Unknown message type $messageType")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTimedEventMessage(timedEvent: TimedEvent) = when (timedEvent) {
|
||||
is TimedEvent.Callback -> timedEvent.callback()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user