192 lines
4.2 KiB
Go
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
|
|
}
|