package main import ( "context" "errors" "fmt" "time" "mxl-player/internal/playback" ) const playlistEventInterval = 10 * time.Millisecond var ( ErrPlayerPlaybackRequired = errors.New("player playback is required") ErrPlayerSessionControllerRequired = errors.New("player session controller is required") ErrPlayerStatusStoreRequired = errors.New("player status store is required") ) type playerPlaylist struct { Controller *playback.PlaylistController // commands is written by GUI-facing methods and consumed by Controller. // events is written by coordinator and consumed by Controller. Run owns // both goroutine lifecycles; cancellation replaces channel closing. coordinator *playback.PlaylistEventCoordinator commands chan playback.PlaylistCommand events chan playback.PlaylistEvent } func newPlayerPlaylist( playlist playback.Playlist, retry playback.RetryPolicy, player *playerPlayback, ) (*playerPlaylist, error) { if player == nil { return nil, ErrPlayerPlaybackRequired } if player.Controller == nil { return nil, ErrPlayerSessionControllerRequired } if player.Status == nil { return nil, ErrPlayerStatusStoreRequired } commands := make(chan playback.PlaylistCommand, 32) events := make(chan playback.PlaylistEvent, 8) controller, err := playback.NewPlaylistController( playlist, retry, player.Commands, ) if err != nil { return nil, err } coordinator, err := playback.NewPlaylistEventCoordinator( controller, player.Controller, player.Status, events, playlistEventInterval, ) if err != nil { return nil, err } return &playerPlaylist{ Controller: controller, coordinator: coordinator, commands: commands, events: events, }, nil } func (p *playerPlaylist) Run(ctx context.Context) error { runCtx, cancel := context.WithCancel(ctx) defer cancel() results := make(chan error, 2) go func() { results <- p.Controller.Run(runCtx, p.commands, p.events) }() go func() { results <- p.coordinator.Run(runCtx) }() first := <-results cancel() second := <-results if ctx.Err() != nil { return ctx.Err() } if err := playlistRuntimeError(first); err != nil { return err } if err := playlistRuntimeError(second); err != nil { return err } return nil } func (p *playerPlaylist) Select(index int) bool { return p.enqueue(playback.PlaylistCommand{ Kind: playback.PlaylistSelect, Index: index, }) } func (p *playerPlaylist) Next() bool { return p.enqueue(playback.PlaylistCommand{Kind: playback.PlaylistNext}) } func (p *playerPlaylist) Previous() bool { return p.enqueue(playback.PlaylistCommand{Kind: playback.PlaylistPrevious}) } func (p *playerPlaylist) Pause() bool { return p.enqueue(playback.PlaylistCommand{Kind: playback.PlaylistPause}) } func (p *playerPlaylist) Resume() bool { return p.enqueue(playback.PlaylistCommand{Kind: playback.PlaylistResume}) } func (p *playerPlaylist) enqueue(command playback.PlaylistCommand) bool { select { case p.commands <- command: return true default: return false } } func playlistRuntimeError(err error) error { if err == nil || errors.Is(err, context.Canceled) { return nil } return err } func shouldAutoStartPlaylist( args appArgs, playlist playback.Playlist, ) bool { return args.PlaylistPath != "" && args.VideoFlowId == "" && args.AudioFlowId == "" && len(playlist.Entries) > 0 } func playlistEntryDisplayName(entry playback.PlaylistEntry, index int) string { if entry.Name != "" { return entry.Name } return fmt.Sprintf("Entry %d", index+1) } func playlistTimingProgress( timing playback.PlaylistTimingState, now time.Time, ) (float32, time.Duration) { if timing.Duration <= 0 { return 0, 0 } if timing.Expired { return 1, 0 } remaining := timing.Remaining if timing.Paused { // Retain the remaining time captured when the owned timer stopped. } else if !timing.Started { return 0, timing.Duration } else { remaining = timing.Deadline.Sub(now) } if remaining < 0 { remaining = 0 } if remaining > timing.Duration { remaining = timing.Duration } fraction := 1 - float32(remaining)/float32(timing.Duration) if fraction < 0 { fraction = 0 } if fraction > 1 { fraction = 1 } return fraction, remaining }