use state machine to manage player state

This commit is contained in:
aynakeya
2025-08-07 01:09:07 +08:00
parent 3aebdb00f9
commit 5a699a1e2e
9 changed files with 105 additions and 50 deletions

View File

@@ -29,7 +29,7 @@ var EventsMapping = map[event.EventId]any{
PlayerPlayingUpdate: PlayerPlayingUpdateEvent{},
PlayerPropertyPauseUpdate: PlayerPropertyPauseUpdateEvent{},
PlayerPropertyPercentPosUpdate: PlayerPropertyPercentPosUpdateEvent{},
PlayerPropertyIdleActiveUpdate: PlayerPropertyIdleActiveUpdateEvent{},
PlayerPropertyStateUpdate: PlayerPropertyStateUpdateEvent{},
PlayerPropertyTimePosUpdate: PlayerPropertyTimePosUpdateEvent{},
PlayerPropertyDurationUpdate: PlayerPropertyDurationUpdateEvent{},
PlayerPropertyVolumeUpdate: PlayerPropertyVolumeUpdateEvent{},

View File

@@ -21,10 +21,10 @@ type PlayerPropertyPercentPosUpdateEvent struct {
PercentPos float64
}
const PlayerPropertyIdleActiveUpdate = "update.player.property.idle_active"
const PlayerPropertyStateUpdate = "update.player.property.state"
type PlayerPropertyIdleActiveUpdateEvent struct {
IsIdle bool
type PlayerPropertyStateUpdateEvent struct {
State model.PlayerState
}
const PlayerPropertyTimePosUpdate = "update.player.property.time_pos"

View File

@@ -4,3 +4,27 @@ type AudioDevice struct {
Name string
Description string
}
type PlayerState int
const (
PlayerStatePlaying PlayerState = iota
PlayerStateLoading
PlayerStateIdle
)
func (s PlayerState) NextState(next PlayerState) PlayerState {
if s == PlayerStatePlaying {
return next
}
if s == PlayerStateIdle {
return next
}
if s == PlayerStateLoading {
if next != PlayerStatePlaying {
return PlayerStateLoading
}
return next
}
return next
}

5
go.mod
View File

@@ -43,6 +43,7 @@ require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/XiaoMengXinX/Music163Api-Go v0.1.30 // indirect
github.com/abadojack/whatlanggo v1.0.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aynakeya/deepcolor v1.0.3 // indirect
@@ -58,7 +59,7 @@ require (
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
@@ -80,7 +81,7 @@ require (
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

13
go.sum
View File

@@ -11,6 +11,8 @@ github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiU
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/XiaoMengXinX/Music163Api-Go v0.1.30 h1:MqRItDFtX1J0JTlFtwN2RwjsYMA7/g/+cTjcOJXy19s=
github.com/XiaoMengXinX/Music163Api-Go v0.1.30/go.mod h1:kLU/CkLxKnEJFCge0URvQ0lHt6ImoG1/2aVeNbgV2RQ=
github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4=
github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc=
github.com/adrg/libvlc-go/v3 v3.1.6 h1:Cm22w6xNMDdzYCW8koHgAvjonYm4xbPP5TrlVTtMdl4=
github.com/adrg/libvlc-go/v3 v3.1.6/go.mod h1:xJK0YD8cyMDejnrTFQinStE6RYCV1nlfS8KmqTpszSc=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
@@ -70,8 +72,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
@@ -80,8 +82,8 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/k0kubun/pp/v3 v3.4.1 h1:1WdFZDRRqe8UsR61N/2RoOZ3ziTEqgTPVqKrHeb779Y=
github.com/k0kubun/pp/v3 v3.4.1/go.mod h1:+SiNiqKnBfw1Nkj82Lh5bIeKQOAkPy6Xw9CAZUZ8npI=
github.com/k0kubun/pp/v3 v3.5.0 h1:iYNlYA5HJAJvkD4ibuf9c8y6SHM0QFhaBuCqm1zHp0w=
github.com/k0kubun/pp/v3 v3.5.0/go.mod h1:5lzno5ZZeEeTV/Ky6vs3g6d1U3WarDrH8k240vMtGro=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -237,8 +239,9 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -2,6 +2,7 @@ package gui
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/gui/gutil"
@@ -84,9 +85,9 @@ func registerPlayControllerHandler() {
PlayController.Progress.Refresh()
}))
global.EventManager.RegisterA(events.PlayerPropertyIdleActiveUpdate, "gui.player.controller.idle_active", gutil.ThreadSafeHandler(func(event *event.Event) {
isIdle := event.Data.(events.PlayerPropertyIdleActiveUpdateEvent).IsIdle
if isIdle {
global.EventManager.RegisterA(events.PlayerPropertyStateUpdate, "gui.player.controller.idle_active", gutil.ThreadSafeHandler(func(event *event.Event) {
state := event.Data.(events.PlayerPropertyStateUpdateEvent).State
if state == model.PlayerStateIdle || state == model.PlayerStateLoading {
PlayController.Progress.Value = 0
PlayController.Progress.Max = 0
//PlayController.Title.SetText("Title")
@@ -178,7 +179,6 @@ func registerPlayControllerHandler() {
PlayController.Cover.Resource = pic.Resource
gutil.RunInFyneThread(PlayController.Cover.Refresh)
}
}()
}
}))

View File

@@ -21,25 +21,39 @@ func Stop() {
func handlePlayNext() {
log := global.Logger.WithPrefix("Controller")
isIdle := false
playerState := model.PlayerStatePlaying
global.EventManager.RegisterA(
events.PlayerPropertyIdleActiveUpdate,
events.PlayerPropertyStateUpdate,
"internal.controller.playcontrol.idleplaynext",
func(event *event.Event) {
data := event.Data.(events.PlayerPropertyIdleActiveUpdateEvent)
isIdle = data.IsIdle
if data.IsIdle {
data := event.Data.(events.PlayerPropertyStateUpdateEvent)
log.Debug("[MPV PlayControl] update player to state", playerState, "->", data.State)
playerState = data.State
if playerState == model.PlayerStateIdle {
log.Info("mpv went idle, try play next")
global.EventManager.CallA(events.PlayerPlayNextCmd,
events.PlayerPlayNextCmdEvent{})
}
})
global.EventManager.RegisterA(
events.PlayerPropertyStateUpdate,
"internal.controller.playcontrol.clear_when_idle", func(event *event.Event) {
data := event.Data.(events.PlayerPropertyStateUpdateEvent)
// if is idle, remove playing media
if data.State == model.PlayerStateIdle {
global.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: model.Media{},
Removed: true,
})
}
})
global.EventManager.RegisterA(
events.PlaylistInsertUpdate(model.PlaylistIDPlayer),
"internal.controller.playcontrol.playnext_when_insert.player",
func(event *event.Event) {
if isIdle && config.General.PlayNextOnFail {
if playerState == model.PlayerStateIdle {
global.EventManager.CallA(events.PlayerPlayNextCmd,
events.PlayerPlayNextCmdEvent{})
}
@@ -49,7 +63,7 @@ func handlePlayNext() {
events.PlaylistInsertUpdate(model.PlaylistIDSystem),
"internal.controller.playcontrol.playnext_when_insert.system",
func(event *event.Event) {
if isIdle && config.General.PlayNextOnFail {
if playerState == model.PlayerStateIdle {
global.EventManager.CallA(events.PlayerPlayNextCmd,
events.PlayerPlayNextCmdEvent{})
}
@@ -82,8 +96,9 @@ func handlePlayNext() {
events.PlayerPlayErrorUpdate,
"internal.controller.playcontrol.playnext_on_error",
func(event *event.Event) {
if isIdle && config.General.PlayNextOnFail {
if config.General.PlayNextOnFail {
global.EventManager.CallA(events.PlayerPlayNextCmd, events.PlayerPlayNextCmdEvent{})
return
}
})

View File

@@ -95,6 +95,7 @@ func StopPlayer() {
var prevPercentPos float64 = 0
var prevTimePos float64 = 0
var currentState = model.PlayerStateIdle
var mpvPropertyHandler = map[string]func(value any){
"pause": func(value any) {
@@ -123,20 +124,20 @@ var mpvPropertyHandler = map[string]func(value any){
},
"idle-active": func(value any) {
var data events.PlayerPropertyIdleActiveUpdateEvent
var data events.PlayerPropertyStateUpdateEvent
if value == nil {
data.IsIdle = true
data.State = model.PlayerStateIdle
} else {
data.IsIdle = value.(bool)
if value.(bool) {
data.State = model.PlayerStateIdle
} else {
data.State = model.PlayerStatePlaying
}
}
// if is idle, remove playing media
if data.IsIdle {
global.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: model.Media{},
Removed: true,
})
}
global.EventManager.CallA(events.PlayerPropertyIdleActiveUpdate, data)
log.Debugf("mpv state update %v + %v = %v", currentState, data.State, currentState.NextState(data.State))
currentState = currentState.NextState(data.State)
data.State = currentState
global.EventManager.CallA(events.PlayerPropertyStateUpdate, data)
},
"time-pos": func(value any) {
@@ -190,15 +191,28 @@ func registerHandler() {
func registerCmdHandler() {
global.EventManager.RegisterA(events.PlayerPlayCmd, "player.play", func(evnt *event.Event) {
currentState = currentState.NextState(model.PlayerStateLoading)
global.EventManager.CallA(
events.PlayerPropertyStateUpdate,
events.PlayerPropertyStateUpdateEvent{
State: currentState,
})
mediaInfo := evnt.Data.(events.PlayerPlayCmdEvent).Media.Info
media := evnt.Data.(events.PlayerPlayCmdEvent).Media
if m, err := miaosic.GetMediaInfo(media.Info.Meta); err == nil {
media.Info = m
}
global.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: media,
Removed: false,
})
log.Infof("[MPV Player] Play media %s", mediaInfo.Title)
mediaUrls, err := miaosic.GetMediaUrl(mediaInfo.Meta, miaosic.QualityAny)
if err != nil || len(mediaUrls) == 0 {
log.Warn("[MPV PlayControl] get media url failed ", mediaInfo.Meta.ID(), err)
global.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: evnt.Data.(events.PlayerPlayCmdEvent).Media,
Removed: false,
})
if err := libmpv.Command([]string{"stop"}); err != nil {
log.Error("[MPV PlayControl] failed to stop", err)
}
global.EventManager.CallA(
events.PlayerPlayErrorUpdate,
events.PlayerPlayErrorUpdateEvent{
@@ -224,14 +238,6 @@ func registerCmdHandler() {
return
}
}
media := evnt.Data.(events.PlayerPlayCmdEvent).Media
if m, err := miaosic.GetMediaInfo(media.Info.Meta); err == nil {
media.Info = m
}
global.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: media,
Removed: false,
})
log.Debugf("mpv command loadfile %s %s", mediaInfo.Title, mediaUrl.Url)
cmd := []string{"loadfile", mediaUrl.Url}
if cfg.DisplayMusicCover && media.Info.Cover.Url != "" {
@@ -254,6 +260,12 @@ func registerCmdHandler() {
})
return
}
currentState = currentState.NextState(model.PlayerStatePlaying)
global.EventManager.CallA(
events.PlayerPropertyStateUpdate,
events.PlayerPropertyStateUpdateEvent{
State: currentState,
})
global.EventManager.CallA(events.PlayerPropertyTimePosUpdate, events.PlayerPropertyTimePosUpdateEvent{
TimePos: 0,
})

View File

@@ -133,8 +133,8 @@ func StopPlayer() {
func registerEvents() {
// 播放结束事件
_, err := eventManager.Attach(vlc.MediaPlayerEndReached, func(e vlc.Event, userData interface{}) {
global.EventManager.CallA(events.PlayerPropertyIdleActiveUpdate, events.PlayerPropertyIdleActiveUpdateEvent{
IsIdle: true,
global.EventManager.CallA(events.PlayerPropertyStateUpdate, events.PlayerPropertyStateUpdateEvent{
State: model.PlayerStateIdle,
})
global.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: model.Media{},
@@ -300,8 +300,8 @@ func registerCmdHandler() {
global.EventManager.CallA(events.PlayerPropertyPercentPosUpdate, events.PlayerPropertyPercentPosUpdateEvent{
PercentPos: 0,
})
global.EventManager.CallA(events.PlayerPropertyIdleActiveUpdate, events.PlayerPropertyIdleActiveUpdateEvent{
IsIdle: false,
global.EventManager.CallA(events.PlayerPropertyStateUpdate, events.PlayerPropertyStateUpdateEvent{
State: model.PlayerStatePlaying,
})
})