mirror of
https://github.com/AynaLivePlayer/AynaLivePlayer.git
synced 2025-12-06 10:22:50 +08:00
368 lines
11 KiB
Go
368 lines
11 KiB
Go
package mpv
|
|
|
|
import (
|
|
"AynaLivePlayer/core/events"
|
|
"AynaLivePlayer/core/model"
|
|
"AynaLivePlayer/global"
|
|
"AynaLivePlayer/pkg/config"
|
|
"AynaLivePlayer/pkg/event"
|
|
"AynaLivePlayer/pkg/logger"
|
|
"AynaLivePlayer/pkg/util"
|
|
"fmt"
|
|
"github.com/AynaLivePlayer/miaosic"
|
|
"github.com/aynakeya/go-mpv"
|
|
"github.com/tidwall/gjson"
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
var running bool = false
|
|
var libmpv *mpv.Mpv = nil
|
|
var log logger.ILogger = nil
|
|
var mpvClientVersion uint32 = 0
|
|
|
|
func SetupPlayer() {
|
|
running = true
|
|
config.LoadConfig(cfg)
|
|
libmpv = mpv.Create()
|
|
log = global.Logger.WithPrefix("MPV Player")
|
|
err := libmpv.Initialize()
|
|
if err != nil {
|
|
log.Error("initialize libmpv failed")
|
|
return
|
|
}
|
|
mpvClientVersion = mpv.ClientApiVersion()
|
|
log.Infof("libmpv version %d", mpv.ClientApiVersion())
|
|
_ = libmpv.SetOptionString("vo", "null")
|
|
log.Info("initialize libmpv success")
|
|
registerHandler()
|
|
registerCmdHandler()
|
|
restoreConfig()
|
|
updateAudioDeviceList()
|
|
log.Info("starting mpv player")
|
|
go func() {
|
|
for running {
|
|
e := libmpv.WaitEvent(1)
|
|
if e == nil {
|
|
log.Warn("[MPV Player] event loop got nil event")
|
|
}
|
|
if e.EventId == mpv.EVENT_PROPERTY_CHANGE {
|
|
eventProperty := e.Property()
|
|
handler, ok := mpvPropertyHandler[eventProperty.Name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
var value interface{} = nil
|
|
if eventProperty.Data != nil {
|
|
value = eventProperty.Data.(mpv.Node).Value
|
|
}
|
|
//log.Debugf("[MPV Player] property update %s %v", eventProperty.Name, value)
|
|
handler(value)
|
|
}
|
|
if e.EventId == mpv.EVENT_SHUTDOWN {
|
|
log.Info("[MPV Player] libmpv shutdown")
|
|
// should not call, otherwise StopPlayer gonna be call twice and cause panic
|
|
// StopPlayer()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func StopPlayer() {
|
|
cfg.AudioDevice = libmpv.GetPropertyString("audio-device")
|
|
log.Debugf("successfully get audio-device and set config %s", cfg.AudioDevice)
|
|
log.Info("stopping mpv player")
|
|
running = false
|
|
done := make(chan struct{})
|
|
|
|
// Stop player async but wait for at most 1 second
|
|
go func() {
|
|
// todo: when call TerminateDestroy after wid has been set, a c code panic will arise.
|
|
// maybe because the window mpv attach to has been closed. so handle was closed twice
|
|
// for now. just don't destroy it. because it also might fix configuration
|
|
// not properly saved issue
|
|
libmpv.TerminateDestroy()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
log.Info("mpv player stopped")
|
|
case <-time.After(2 * time.Second):
|
|
log.Error("mpv player stop timed out (2s) ")
|
|
}
|
|
}
|
|
|
|
var prevPercentPos float64 = 0
|
|
var prevTimePos float64 = 0
|
|
var currentState = model.PlayerStateIdle
|
|
|
|
var mpvPropertyHandler = map[string]func(value any){
|
|
"pause": func(value any) {
|
|
var data events.PlayerPropertyPauseUpdateEvent
|
|
log.Debugf("pause property update %v %T", value, value)
|
|
data.Paused = value.(bool)
|
|
global.EventManager.CallA(events.PlayerPropertyPauseUpdate, data)
|
|
},
|
|
"percent-pos": func(value any) {
|
|
var data events.PlayerPropertyPercentPosUpdateEvent
|
|
if value == nil {
|
|
data.PercentPos = 0
|
|
} else {
|
|
data.PercentPos = value.(float64)
|
|
}
|
|
// ignore bug value
|
|
if data.PercentPos < 0.1 {
|
|
return
|
|
}
|
|
// ignore small change
|
|
if math.Abs(data.PercentPos-prevPercentPos) < 0.5 {
|
|
return
|
|
}
|
|
prevPercentPos = data.PercentPos
|
|
global.EventManager.CallA(events.PlayerPropertyPercentPosUpdate, data)
|
|
|
|
},
|
|
"idle-active": func(value any) {
|
|
var data events.PlayerPropertyStateUpdateEvent
|
|
if value == nil {
|
|
data.State = model.PlayerStateIdle
|
|
} else {
|
|
if value.(bool) {
|
|
data.State = model.PlayerStateIdle
|
|
} else {
|
|
data.State = model.PlayerStatePlaying
|
|
}
|
|
}
|
|
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) {
|
|
var data events.PlayerPropertyTimePosUpdateEvent
|
|
if value == nil {
|
|
data.TimePos = 0
|
|
} else {
|
|
data.TimePos = value.(float64)
|
|
}
|
|
// ignore bug value
|
|
if data.TimePos < 0.1 {
|
|
return
|
|
}
|
|
// ignore small change
|
|
if math.Abs(data.TimePos-prevTimePos) < 0.5 {
|
|
return
|
|
}
|
|
prevTimePos = data.TimePos
|
|
global.EventManager.CallA(events.PlayerPropertyTimePosUpdate, data)
|
|
},
|
|
"duration": func(value any) {
|
|
var data events.PlayerPropertyDurationUpdateEvent
|
|
if value == nil {
|
|
data.Duration = 0
|
|
} else {
|
|
data.Duration = value.(float64)
|
|
}
|
|
global.EventManager.CallA(events.PlayerPropertyDurationUpdate, data)
|
|
},
|
|
"volume": func(value any) {
|
|
var data events.PlayerPropertyVolumeUpdateEvent
|
|
if value == nil {
|
|
data.Volume = 0
|
|
} else {
|
|
data.Volume = value.(float64)
|
|
}
|
|
global.EventManager.CallA(events.PlayerPropertyVolumeUpdate, data)
|
|
},
|
|
}
|
|
|
|
func registerHandler() {
|
|
var err error
|
|
for property, _ := range mpvPropertyHandler {
|
|
log.Infof("register handler for mpv property %s", property)
|
|
err = libmpv.ObserveProperty(util.Hash64(property), property, mpv.FORMAT_NODE)
|
|
if err != nil {
|
|
log.Errorf("register handler for mpv property %s failed: %s", property, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
if err := libmpv.Command([]string{"stop"}); err != nil {
|
|
log.Error("[MPV PlayControl] failed to stop", err)
|
|
}
|
|
global.EventManager.CallA(
|
|
events.PlayerPlayErrorUpdate,
|
|
events.PlayerPlayErrorUpdateEvent{
|
|
Error: err,
|
|
})
|
|
return
|
|
}
|
|
mediaUrl := mediaUrls[0]
|
|
if val, ok := mediaUrl.Header["User-Agent"]; ok {
|
|
log.Debug("[MPV PlayControl] set user-agent for mpv player")
|
|
err := libmpv.SetPropertyString("user-agent", val)
|
|
if err != nil {
|
|
log.Warn("[MPV PlayControl] set player user-agent failed", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if val, ok := mediaUrl.Header["Referer"]; ok {
|
|
log.Debug("[MPV PlayControl] set referrer for mpv player")
|
|
err := libmpv.SetPropertyString("referrer", val)
|
|
if err != nil {
|
|
log.Warn("[MPV PlayControl] set player referrer failed", err)
|
|
return
|
|
}
|
|
}
|
|
log.Debugf("mpv command loadfile %s %s", mediaInfo.Title, mediaUrl.Url)
|
|
cmd := []string{"loadfile", mediaUrl.Url}
|
|
if cfg.DisplayMusicCover && media.Info.Cover.Url != "" {
|
|
// add media cover to video channel.
|
|
// https://mpv.io/manual/master/#command-interface-[<options>]]]
|
|
// api changes after client version 2.3 (0.38.0
|
|
if mpvClientVersion >= ((2 << 16) | 3) {
|
|
cmd = append(cmd, "replace", "0", "external-files-append=\""+media.Info.Cover.Url+"\",vid=1")
|
|
} else {
|
|
cmd = append(cmd, "replace", "external-files-append=\""+media.Info.Cover.Url+"\",vid=1")
|
|
}
|
|
}
|
|
log.Debug("[MPV PlayControl] mpv command", cmd)
|
|
if err := libmpv.Command(cmd); err != nil {
|
|
log.Error("[MPV PlayControl] mpv load media failed", cmd, mediaInfo, err)
|
|
global.EventManager.CallA(
|
|
events.PlayerPlayErrorUpdate,
|
|
events.PlayerPlayErrorUpdateEvent{
|
|
Error: err,
|
|
})
|
|
return
|
|
}
|
|
currentState = currentState.NextState(model.PlayerStatePlaying)
|
|
global.EventManager.CallA(
|
|
events.PlayerPropertyStateUpdate,
|
|
events.PlayerPropertyStateUpdateEvent{
|
|
State: currentState,
|
|
})
|
|
global.EventManager.CallA(events.PlayerPropertyTimePosUpdate, events.PlayerPropertyTimePosUpdateEvent{
|
|
TimePos: 0,
|
|
})
|
|
global.EventManager.CallA(events.PlayerPropertyPercentPosUpdate, events.PlayerPropertyPercentPosUpdateEvent{
|
|
PercentPos: 0,
|
|
})
|
|
})
|
|
global.EventManager.RegisterA(events.PlayerToggleCmd, "player.toggle", func(evnt *event.Event) {
|
|
property, err := libmpv.GetProperty("pause", mpv.FORMAT_FLAG)
|
|
if err != nil {
|
|
log.Warn("[MPV PlayControl] get property pause failed", err)
|
|
return
|
|
}
|
|
err = libmpv.SetProperty("pause", mpv.FORMAT_FLAG, !property.(bool))
|
|
if err != nil {
|
|
log.Warn("[MPV PlayControl] toggle pause failed", err)
|
|
}
|
|
})
|
|
global.EventManager.RegisterA(events.PlayerSetPauseCmd, "player.set_paused", func(evnt *event.Event) {
|
|
data := evnt.Data.(events.PlayerSetPauseCmdEvent)
|
|
err := libmpv.SetProperty("pause", mpv.FORMAT_FLAG, data.Pause)
|
|
if err != nil {
|
|
log.Warn("[MPV PlayControl] set pause failed", err)
|
|
}
|
|
})
|
|
global.EventManager.RegisterA(events.PlayerSeekCmd, "player.seek", func(evnt *event.Event) {
|
|
data := evnt.Data.(events.PlayerSeekCmdEvent)
|
|
log.Debugf("seek to %f (absolute=%t)", data.Position, data.Absolute)
|
|
var err error
|
|
if data.Absolute {
|
|
err = libmpv.SetProperty("time-pos", mpv.FORMAT_DOUBLE, data.Position)
|
|
} else {
|
|
err = libmpv.SetProperty("percent-pos", mpv.FORMAT_DOUBLE, data.Position)
|
|
}
|
|
if err != nil {
|
|
log.Warn("seek failed", err)
|
|
}
|
|
})
|
|
global.EventManager.RegisterA(events.PlayerVolumeChangeCmd, "player.volume", func(evnt *event.Event) {
|
|
data := evnt.Data.(events.PlayerVolumeChangeCmdEvent)
|
|
err := libmpv.SetProperty("volume", mpv.FORMAT_DOUBLE, data.Volume)
|
|
if err != nil {
|
|
log.Warn("set volume failed", err)
|
|
}
|
|
})
|
|
|
|
global.EventManager.RegisterA(events.PlayerVideoPlayerSetWindowHandleCmd, "player.set_window_handle", func(evnt *event.Event) {
|
|
handle := evnt.Data.(events.PlayerVideoPlayerSetWindowHandleCmdEvent).Handle
|
|
err := SetWindowHandle(handle)
|
|
if err != nil {
|
|
log.Warn("set window handle failed", err)
|
|
}
|
|
})
|
|
|
|
global.EventManager.RegisterA(events.PlayerSetAudioDeviceCmd, "player.set_audio_device", func(evnt *event.Event) {
|
|
device := evnt.Data.(events.PlayerSetAudioDeviceCmdEvent).Device
|
|
err := libmpv.SetPropertyString("audio-device", device)
|
|
if err != nil {
|
|
global.EventManager.CallA(
|
|
events.ErrorUpdate,
|
|
events.ErrorUpdateEvent{
|
|
Error: err,
|
|
})
|
|
log.Warn("set audio device failed", err)
|
|
}
|
|
log.Infof("set audio device to %s", device)
|
|
return
|
|
})
|
|
}
|
|
|
|
func SetWindowHandle(handle uintptr) error {
|
|
log.Infof("set window handle %d", handle)
|
|
_ = libmpv.SetOptionString("wid", fmt.Sprintf("%d", handle))
|
|
return libmpv.SetOptionString("vo", "gpu")
|
|
}
|
|
|
|
// // updateAudioDeviceList get output device for mpv
|
|
// // return format is []AudioDevice
|
|
func updateAudioDeviceList() {
|
|
property, err := libmpv.GetProperty("audio-device-list", mpv.FORMAT_STRING)
|
|
if err != nil {
|
|
return
|
|
}
|
|
ad := libmpv.GetPropertyString("audio-device")
|
|
dl := make([]model.AudioDevice, 0)
|
|
gjson.Parse(property.(string)).ForEach(func(key, value gjson.Result) bool {
|
|
dl = append(dl, model.AudioDevice{
|
|
Name: value.Get("name").String(),
|
|
Description: value.Get("description").String(),
|
|
})
|
|
return true
|
|
})
|
|
log.Infof("update audio device list %v, current %s", dl, ad)
|
|
global.EventManager.CallA(events.PlayerAudioDeviceUpdate, events.PlayerAudioDeviceUpdateEvent{
|
|
Current: ad,
|
|
Devices: dl,
|
|
})
|
|
return
|
|
}
|