Files
go-mxl-player/cmd/mxl-player/playlist_runtime.go
T
2026-09-02 00:08:53 +03:00

192 lines
4.2 KiB
Go

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
}