autoadvance

package
v2.22.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 9, 2026 License: GPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package autoadvance moves playlists on without anyone pressing next.

A playlist item is "bound" when the launch that played it published media in its slot: only then does the engine know that when that media ends, the item is what ended, and only then may it act. Media that appeared any other way, such as a game started from the menu, is never bound and is never touched. Every automatic move is also guarded: the playlist must still be the active one, on the same item, playing and not stopped by a failure, so a user who got there first always wins.

What happens when bound media ends depends on why it ended. Media that finished moves the playlist on at once. A game that exited by itself gets a cancellable "up next" countdown, or, if it was started automatically and quit within seconds, pauses the playlist instead, since the user plainly did not want it. Anything Core stopped or replaced never advances.

Two timers rotate a playlist while its media is still running. An advance timer runs from the moment an item's media appears and freezes while the playlist is paused. An idle timer measures time without input and only runs where the platform reports keyboard, mouse and controller activity; it starts a loaded ("armed") playlist, rotates a playing one, and carries on a playlist whose game has exited, but never rotates away from media the playlist did not launch.

All state belongs to one goroutine. Observer callbacks and the other inputs only append to an unbounded queue, so they never block the code that reports them, and no event is ever dropped. Deadlines are never trusted to a timer firing: after every event and every wakeup the loop compares each deadline with the clock, so a late or spurious wakeup does no harm.

Index

Constants

This section is empty.

Variables

View Source
var ErrIdleUnsupported = errors.New(
	"idle rotation is not available: this device cannot detect keyboard, mouse and controller activity",
)

ErrIdleUnsupported is returned when idle rotation is asked for on a device that cannot see all of the input it would have to wait for.

Functions

func IdleSupported

func IdleSupported(pl platforms.Platform) bool

IdleSupported reports whether the platform can detect keyboard, mouse and controller activity, which idle rotation needs: without all three it would switch games under someone who is still playing.

func ReconcileMove added in v2.22.0

func ReconcileMove(active, move *playlists.Playlist) *playlists.Playlist

ReconcileMove returns the move to make for an automatic move that was decided before the repeat mode of active changed, such as to repeat-one while the move waited in the queue. The move keeps the stamps that let the queue handler drop it when overtaken. A move decided under the current repeat mode, one from an armed playlist, and one that stops the playlist on request come back unchanged.

Types

type Config

type Config struct {
	Clock clockwork.Clock
	Host  Host
	// UI opens countdown prompts. It may be nil.
	UI UIOpener
	// Input reports user input. It may be nil, in which case input never
	// cancels a countdown and idle rotation cannot run.
	Input platforms.InputActivitySource
	// PublishesLater reports whether media from the launcher with this ID is
	// published by an external tracker after the launch call returns. Media
	// that waits on a launch is bound only if it says so. Nil accepts any
	// launcher.
	PublishesLater func(launcherID string) bool
	// HasRenderer reports that the platform shows UI events itself. Without
	// one a countdown before a rotation is skipped and the switch is made
	// silently; the countdown after an exit runs either way, because a client
	// may answer it.
	HasRenderer bool
}

Config wires an Engine.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is the auto-advance state machine. Create one with New. A nil *Engine accepts every call and does nothing, so callers never need to check whether it is wired.

func New

func New(cfg *Config) *Engine

New creates an engine. Call Start to run it.

func (*Engine) ItemLaunched

func (e *Engine) ItemLaunched(pls *playlists.Playlist, err error, publishesLater bool)

ItemLaunched reports that the launch started by ItemLaunching has returned.

publishesLater says the item's launcher publishes its media itself after the launch call returns, as external lifecycle launchers do. For those the first primary media to appear in the next 30 seconds is bound to the item; for any other launcher media that did not appear during the launch is not the item's.

func (*Engine) ItemLaunching

func (e *Engine) ItemLaunching(pls *playlists.Playlist)

ItemLaunching reports that the launch of the item pls is on is starting. Media published until ItemLaunched is bound to the item.

func (*Engine) ItemMoved added in v2.22.0

func (e *Engine) ItemMoved(slot, plsID string, from, to int)

ItemMoved reports that the item at index from of the playlist plsID in slot is now at index to, without being launched again, as when shuffle is turned on or off under it. Whatever the engine has bound to the item follows it, so the item still advances when it ends, and a countdown for it carries on. from and to may be equal: the change then only keeps what is waiting on the item. Post it before the PlaylistChanged of the same change.

func (*Engine) ItemRefused

func (e *Engine) ItemRefused(pls *playlists.Playlist)

ItemRefused reports that the queue handler refused the engine's update pls because another launch was still under way. The engine tries the move again shortly, if it is still wanted.

func (*Engine) ItemRemoved added in v2.22.0

func (e *Engine) ItemRemoved(slot, plsID string, index int)

ItemRemoved reports that the item at index of the playlist plsID in slot is gone from it while the playlist stays open, as when a deck is edited. The index now names another item, so nothing bound to the old one may follow it. Post it before the PlaylistChanged of the same change.

func (*Engine) MediaEnded

func (e *Engine) MediaEnded(end state.MediaEnd)

MediaEnded implements state.MediaObserver.

func (*Engine) MediaStarted

func (e *Engine) MediaStarted(start state.MediaStart)

MediaStarted implements state.MediaObserver.

func (*Engine) NoteUserActivity

func (e *Engine) NoteUserActivity()

NoteUserActivity reports a token scan or removal.

func (*Engine) PlaylistChanged

func (e *Engine) PlaylistChanged(slot string)

PlaylistChanged reports that the playlist in slot was replaced, cleared or updated.

func (*Engine) Start

func (e *Engine) Start(ctx context.Context)

Start runs the engine until ctx is cancelled or Stop is called.

func (*Engine) Stop

func (e *Engine) Stop()

Stop ends the engine and waits for everything it started.

func (*Engine) Sync

func (e *Engine) Sync()

Sync waits until every event posted before the call has been handled and the engine has acted on it. It exists so tests can assert that nothing happened without sleeping.

type Host

type Host interface {
	// ActivePlaylist and BackgroundPlaylist return the playlist open in each
	// slot, or nil.
	ActivePlaylist() *playlists.Playlist
	BackgroundPlaylist() *playlists.Playlist
	// ActiveMediaGeneration returns the generation of the primary media
	// playing now.
	ActiveMediaGeneration() (gen uint64, ok bool)
	// PausePlaylistIfCurrent pauses pls in slot if it is still the open one.
	PausePlaylistIfCurrent(slot string, pls *playlists.Playlist) bool
	// StopSeq counts the stops of primary media Core was asked to make, once
	// when each starts and again when it completes.
	StopSeq() uint64
	// StopInProgress reports whether a stop of the primary media is running.
	StopInProgress() bool
	// Enqueue sends an update to the playlist queue, giving up when ctx ends.
	Enqueue(ctx context.Context, pls *playlists.Playlist) bool
}

Host is what the engine needs from the rest of the service.

type UIOpener

type UIOpener interface {
	Open(ctx context.Context, request *uievents.Request) (*uievents.Handle, error)
	// State reports the UI events open now, so a prompt never replaces one
	// that is not the engine's.
	State() models.UIStateResponse
}

UIOpener shows a UI event to the user.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL