mirror of
https://github.com/AynaLivePlayer/AynaLivePlayer.git
synced 2026-03-15 14:03:17 +08:00
add linux smc
This commit is contained in:
2
go.mod
2
go.mod
@@ -22,6 +22,7 @@ require (
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728
|
||||
github.com/go-ole/go-ole v1.3.0
|
||||
github.com/go-resty/resty/v2 v2.17.1
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/mattn/go-colorable v0.1.14
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
@@ -59,7 +60,6 @@ require (
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
|
||||
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.6.0 // indirect
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||
|
||||
@@ -1,9 +1,420 @@
|
||||
package sysmediacontrol
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/core/events"
|
||||
"AynaLivePlayer/core/model"
|
||||
"AynaLivePlayer/global"
|
||||
"AynaLivePlayer/pkg/config"
|
||||
"AynaLivePlayer/pkg/eventbus"
|
||||
"AynaLivePlayer/pkg/logger"
|
||||
"fmt"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/godbus/dbus/v5/introspect"
|
||||
"github.com/godbus/dbus/v5/prop"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
mprisBusName = "org.mpris.MediaPlayer2.AynaLivePlayer"
|
||||
mprisObjPath = dbus.ObjectPath("/org/mpris/MediaPlayer2")
|
||||
mprisRootIF = "org.mpris.MediaPlayer2"
|
||||
mprisPlayerIF = "org.mpris.MediaPlayer2.Player"
|
||||
noTrackPath = dbus.ObjectPath("/org/mpris/MediaPlayer2/TrackList/NoTrack")
|
||||
)
|
||||
|
||||
var (
|
||||
linuxSMCLog logger.ILogger
|
||||
linuxSMC *linuxMpris
|
||||
)
|
||||
|
||||
type linuxMpris struct {
|
||||
conn *dbus.Conn
|
||||
props *prop.Properties
|
||||
mu sync.Mutex
|
||||
|
||||
trackSeq uint64
|
||||
trackPath dbus.ObjectPath
|
||||
metadata map[string]dbus.Variant
|
||||
positionUS int64
|
||||
durationUS int64
|
||||
volume float64
|
||||
playback string
|
||||
}
|
||||
|
||||
type mprisRoot struct{}
|
||||
type mprisPlayer struct{}
|
||||
|
||||
func (m *linuxMpris) nextTrackPath() dbus.ObjectPath {
|
||||
m.trackSeq++
|
||||
return dbus.ObjectPath(fmt.Sprintf("/org/mpris/MediaPlayer2/track/%d", m.trackSeq))
|
||||
}
|
||||
|
||||
func (m *linuxMpris) emitProps(changed map[string]dbus.Variant) {
|
||||
_ = m.conn.Emit(
|
||||
mprisObjPath,
|
||||
"org.freedesktop.DBus.Properties.PropertiesChanged",
|
||||
mprisPlayerIF,
|
||||
changed,
|
||||
[]string{},
|
||||
)
|
||||
}
|
||||
|
||||
func toMprisVolume(v float64) float64 {
|
||||
if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
return v / 100.0
|
||||
}
|
||||
|
||||
func fromMprisVolume(v float64) float64 {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
return v * 100.0
|
||||
}
|
||||
|
||||
func (m *linuxMpris) setPlayback(status string) {
|
||||
m.mu.Lock()
|
||||
m.playback = status
|
||||
props := m.props
|
||||
m.mu.Unlock()
|
||||
if props != nil {
|
||||
props.SetMust(mprisPlayerIF, "PlaybackStatus", status)
|
||||
}
|
||||
m.emitProps(map[string]dbus.Variant{
|
||||
"PlaybackStatus": dbus.MakeVariant(status),
|
||||
})
|
||||
}
|
||||
|
||||
func (m *linuxMpris) setPosition(seconds float64) {
|
||||
pos := int64(seconds * float64(time.Second/time.Microsecond))
|
||||
if pos < 0 {
|
||||
pos = 0
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.positionUS = pos
|
||||
props := m.props
|
||||
m.mu.Unlock()
|
||||
// Keep Position up-to-date for Get(Position), but avoid emitting change signal.
|
||||
// KDE/GNOME estimate progress locally while playing; frequent signal pushes cause jitter.
|
||||
if props != nil {
|
||||
props.SetMust(mprisPlayerIF, "Position", pos)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *linuxMpris) setDuration(seconds float64) {
|
||||
duration := int64(seconds * float64(time.Second/time.Microsecond))
|
||||
if duration < 0 {
|
||||
duration = 0
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.durationUS = duration
|
||||
m.metadata["mpris:length"] = dbus.MakeVariant(duration)
|
||||
md := m.metadata
|
||||
props := m.props
|
||||
m.mu.Unlock()
|
||||
if props != nil {
|
||||
props.SetMust(mprisPlayerIF, "Metadata", md)
|
||||
}
|
||||
m.emitProps(map[string]dbus.Variant{
|
||||
"Metadata": dbus.MakeVariant(md),
|
||||
})
|
||||
}
|
||||
|
||||
func (m *linuxMpris) setVolume(v float64) {
|
||||
mv := toMprisVolume(v)
|
||||
m.mu.Lock()
|
||||
m.volume = mv
|
||||
props := m.props
|
||||
m.mu.Unlock()
|
||||
if props != nil {
|
||||
props.SetMust(mprisPlayerIF, "Volume", mv)
|
||||
}
|
||||
m.emitProps(map[string]dbus.Variant{
|
||||
"Volume": dbus.MakeVariant(mv),
|
||||
})
|
||||
}
|
||||
|
||||
func (m *linuxMpris) setPlaying(data events.PlayerPlayingUpdateEvent) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if data.Removed {
|
||||
m.trackPath = noTrackPath
|
||||
m.metadata = map[string]dbus.Variant{
|
||||
"mpris:trackid": dbus.MakeVariant(noTrackPath),
|
||||
}
|
||||
m.positionUS = 0
|
||||
m.playback = "Stopped"
|
||||
props := m.props
|
||||
if props != nil {
|
||||
props.SetMust(mprisPlayerIF, "PlaybackStatus", m.playback)
|
||||
props.SetMust(mprisPlayerIF, "Metadata", m.metadata)
|
||||
props.SetMust(mprisPlayerIF, "Position", m.positionUS)
|
||||
}
|
||||
m.emitProps(map[string]dbus.Variant{
|
||||
"PlaybackStatus": dbus.MakeVariant(m.playback),
|
||||
"Metadata": dbus.MakeVariant(m.metadata),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
m.trackPath = m.nextTrackPath()
|
||||
metadata := map[string]dbus.Variant{
|
||||
"mpris:trackid": dbus.MakeVariant(m.trackPath),
|
||||
"xesam:title": dbus.MakeVariant(data.Media.Info.Title),
|
||||
"xesam:album": dbus.MakeVariant(data.Media.Info.Album),
|
||||
}
|
||||
if data.Media.Info.Artist != "" {
|
||||
metadata["xesam:artist"] = dbus.MakeVariant([]string{data.Media.Info.Artist})
|
||||
}
|
||||
if data.Media.Info.Cover.Url != "" {
|
||||
metadata["mpris:artUrl"] = dbus.MakeVariant(data.Media.Info.Cover.Url)
|
||||
}
|
||||
if m.durationUS > 0 {
|
||||
metadata["mpris:length"] = dbus.MakeVariant(m.durationUS)
|
||||
}
|
||||
|
||||
m.metadata = metadata
|
||||
m.positionUS = 0
|
||||
m.playback = "Playing"
|
||||
props := m.props
|
||||
if props != nil {
|
||||
props.SetMust(mprisPlayerIF, "PlaybackStatus", m.playback)
|
||||
props.SetMust(mprisPlayerIF, "Metadata", m.metadata)
|
||||
props.SetMust(mprisPlayerIF, "Position", m.positionUS)
|
||||
}
|
||||
m.emitProps(map[string]dbus.Variant{
|
||||
"PlaybackStatus": dbus.MakeVariant(m.playback),
|
||||
"Metadata": dbus.MakeVariant(m.metadata),
|
||||
})
|
||||
}
|
||||
|
||||
func (m *mprisRoot) Raise() *dbus.Error { return nil }
|
||||
func (m *mprisRoot) Quit() *dbus.Error { return nil }
|
||||
|
||||
func (m *mprisPlayer) Next() *dbus.Error {
|
||||
_ = global.EventBus.Publish(events.PlayerPlayNextCmd, events.PlayerPlayNextCmdEvent{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mprisPlayer) Previous() *dbus.Error {
|
||||
_ = global.EventBus.Publish(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
||||
Position: 0,
|
||||
Absolute: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mprisPlayer) Pause() *dbus.Error {
|
||||
_ = global.EventBus.Publish(events.PlayerSetPauseCmd, events.PlayerSetPauseCmdEvent{Pause: true})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mprisPlayer) PlayPause() *dbus.Error {
|
||||
_ = global.EventBus.Publish(events.PlayerToggleCmd, events.PlayerToggleCmdEvent{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mprisPlayer) Stop() *dbus.Error {
|
||||
_ = global.EventBus.Publish(events.PlayerSetPauseCmd, events.PlayerSetPauseCmdEvent{Pause: true})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mprisPlayer) Play() *dbus.Error {
|
||||
_ = global.EventBus.Publish(events.PlayerSetPauseCmd, events.PlayerSetPauseCmdEvent{Pause: false})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mprisPlayer) Seek(offset int64) *dbus.Error {
|
||||
_ = global.EventBus.Publish(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
||||
Position: float64(offset) / float64(time.Second/time.Microsecond),
|
||||
Absolute: false,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mprisPlayer) SetPosition(trackID dbus.ObjectPath, position int64) *dbus.Error {
|
||||
if linuxSMC == nil {
|
||||
return nil
|
||||
}
|
||||
linuxSMC.mu.Lock()
|
||||
currentTrack := linuxSMC.trackPath
|
||||
linuxSMC.mu.Unlock()
|
||||
if currentTrack != noTrackPath && trackID != currentTrack {
|
||||
return nil
|
||||
}
|
||||
_ = global.EventBus.Publish(events.PlayerSeekCmd, events.PlayerSeekCmdEvent{
|
||||
Position: float64(position) / float64(time.Second/time.Microsecond),
|
||||
Absolute: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mprisPlayer) OpenUri(_ string) *dbus.Error { return nil }
|
||||
|
||||
func InitSystemMediaControl() {
|
||||
// stub
|
||||
linuxSMCLog = global.Logger.WithPrefix("SMTC-Linux")
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
linuxSMCLog.Warnf("failed to connect session bus: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
reply, err := conn.RequestName(mprisBusName, dbus.NameFlagDoNotQueue)
|
||||
if err != nil || reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
linuxSMCLog.Warnf("failed to own mpris bus name (%v, %v)", reply, err)
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
linuxSMC = &linuxMpris{
|
||||
conn: conn,
|
||||
trackPath: noTrackPath,
|
||||
metadata: map[string]dbus.Variant{
|
||||
"mpris:trackid": dbus.MakeVariant(noTrackPath),
|
||||
},
|
||||
positionUS: 0,
|
||||
durationUS: 0,
|
||||
volume: 0.5,
|
||||
playback: "Stopped",
|
||||
}
|
||||
|
||||
propsSpec := map[string]map[string]*prop.Prop{
|
||||
mprisRootIF: {
|
||||
"CanQuit": {Value: false, Writable: false, Emit: prop.EmitTrue},
|
||||
"CanRaise": {Value: false, Writable: false, Emit: prop.EmitTrue},
|
||||
"HasTrackList": {Value: false, Writable: false, Emit: prop.EmitTrue},
|
||||
"Identity": {Value: config.ProgramName, Writable: false, Emit: prop.EmitTrue},
|
||||
"DesktopEntry": {Value: "AynaLivePlayer", Writable: false, Emit: prop.EmitTrue},
|
||||
"SupportedUriSchemes": {Value: []string{"file", "http", "https"}, Writable: false, Emit: prop.EmitTrue},
|
||||
"SupportedMimeTypes": {Value: []string{}, Writable: false, Emit: prop.EmitTrue},
|
||||
"Fullscreen": {Value: false, Writable: false, Emit: prop.EmitTrue},
|
||||
"CanSetFullscreen": {Value: false, Writable: false, Emit: prop.EmitTrue},
|
||||
},
|
||||
mprisPlayerIF: {
|
||||
"PlaybackStatus": {Value: linuxSMC.playback, Writable: false, Emit: prop.EmitFalse},
|
||||
"Metadata": {Value: linuxSMC.metadata, Writable: false, Emit: prop.EmitFalse},
|
||||
"Volume": {
|
||||
Value: linuxSMC.volume,
|
||||
Writable: true,
|
||||
Emit: prop.EmitFalse,
|
||||
Callback: func(c *prop.Change) *dbus.Error {
|
||||
v, ok := c.Value.(float64)
|
||||
if !ok {
|
||||
return dbus.MakeFailedError(fmt.Errorf("invalid volume type %T", c.Value))
|
||||
}
|
||||
linuxSMC.setVolume(fromMprisVolume(v))
|
||||
_ = global.EventBus.Publish(events.PlayerVolumeChangeCmd, events.PlayerVolumeChangeCmdEvent{
|
||||
Volume: fromMprisVolume(v),
|
||||
})
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"Position": {Value: linuxSMC.positionUS, Writable: false, Emit: prop.EmitFalse},
|
||||
"CanGoNext": {Value: true, Writable: false, Emit: prop.EmitTrue},
|
||||
"CanGoPrevious": {Value: true, Writable: false, Emit: prop.EmitTrue},
|
||||
"CanPlay": {Value: true, Writable: false, Emit: prop.EmitTrue},
|
||||
"CanPause": {Value: true, Writable: false, Emit: prop.EmitTrue},
|
||||
"CanSeek": {Value: true, Writable: false, Emit: prop.EmitTrue},
|
||||
"CanControl": {Value: true, Writable: false, Emit: prop.EmitTrue},
|
||||
},
|
||||
}
|
||||
|
||||
linuxSMC.props = prop.New(conn, mprisObjPath, propsSpec)
|
||||
_ = conn.Export(&mprisRoot{}, mprisObjPath, mprisRootIF)
|
||||
_ = conn.Export(&mprisPlayer{}, mprisObjPath, mprisPlayerIF)
|
||||
|
||||
node := &introspect.Node{
|
||||
Name: string(mprisObjPath),
|
||||
Interfaces: []introspect.Interface{
|
||||
introspect.IntrospectData,
|
||||
prop.IntrospectData,
|
||||
{
|
||||
Name: mprisRootIF,
|
||||
Methods: []introspect.Method{
|
||||
{Name: "Raise"},
|
||||
{Name: "Quit"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: mprisPlayerIF,
|
||||
Methods: []introspect.Method{
|
||||
{Name: "Next"},
|
||||
{Name: "Previous"},
|
||||
{Name: "Pause"},
|
||||
{Name: "PlayPause"},
|
||||
{Name: "Stop"},
|
||||
{Name: "Play"},
|
||||
{
|
||||
Name: "Seek",
|
||||
Args: []introspect.Arg{{Name: "Offset", Type: "x", Direction: "in"}},
|
||||
},
|
||||
{
|
||||
Name: "SetPosition",
|
||||
Args: []introspect.Arg{
|
||||
{Name: "TrackId", Type: "o", Direction: "in"},
|
||||
{Name: "Position", Type: "x", Direction: "in"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OpenUri",
|
||||
Args: []introspect.Arg{{Name: "Uri", Type: "s", Direction: "in"}},
|
||||
},
|
||||
},
|
||||
Signals: []introspect.Signal{
|
||||
{
|
||||
Name: "Seeked",
|
||||
Args: []introspect.Arg{{Name: "Position", Type: "x"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
_ = conn.Export(introspect.NewIntrospectable(node), mprisObjPath, "org.freedesktop.DBus.Introspectable")
|
||||
|
||||
_ = global.EventBus.Subscribe("", events.PlayerPlayingUpdate, "sysmediacontrol.linux.playing", func(event *eventbus.Event) {
|
||||
linuxSMC.setPlaying(event.Data.(events.PlayerPlayingUpdateEvent))
|
||||
})
|
||||
_ = global.EventBus.Subscribe("", events.PlayerPropertyPauseUpdate, "sysmediacontrol.linux.pause", func(event *eventbus.Event) {
|
||||
if event.Data.(events.PlayerPropertyPauseUpdateEvent).Paused {
|
||||
linuxSMC.setPlayback("Paused")
|
||||
} else {
|
||||
linuxSMC.setPlayback("Playing")
|
||||
}
|
||||
})
|
||||
_ = global.EventBus.Subscribe("", events.PlayerPropertyDurationUpdate, "sysmediacontrol.linux.duration", func(event *eventbus.Event) {
|
||||
linuxSMC.setDuration(event.Data.(events.PlayerPropertyDurationUpdateEvent).Duration)
|
||||
})
|
||||
_ = global.EventBus.Subscribe("", events.PlayerPropertyTimePosUpdate, "sysmediacontrol.linux.timepos", func(event *eventbus.Event) {
|
||||
linuxSMC.setPosition(event.Data.(events.PlayerPropertyTimePosUpdateEvent).TimePos)
|
||||
})
|
||||
_ = global.EventBus.Subscribe("", events.PlayerPropertyVolumeUpdate, "sysmediacontrol.linux.volume", func(event *eventbus.Event) {
|
||||
linuxSMC.setVolume(event.Data.(events.PlayerPropertyVolumeUpdateEvent).Volume)
|
||||
})
|
||||
_ = global.EventBus.Subscribe("", events.PlayerPropertyStateUpdate, "sysmediacontrol.linux.state", func(event *eventbus.Event) {
|
||||
state := event.Data.(events.PlayerPropertyStateUpdateEvent).State
|
||||
if state == model.PlayerStateIdle {
|
||||
linuxSMC.setPlayback("Stopped")
|
||||
}
|
||||
})
|
||||
|
||||
linuxSMCLog.Info("linux MPRIS media control initialized")
|
||||
}
|
||||
|
||||
func Destroy() {
|
||||
// stub
|
||||
if linuxSMC == nil {
|
||||
return
|
||||
}
|
||||
_ = global.EventBus.Unsubscribe(events.PlayerPlayingUpdate, "sysmediacontrol.linux.playing")
|
||||
_ = global.EventBus.Unsubscribe(events.PlayerPropertyPauseUpdate, "sysmediacontrol.linux.pause")
|
||||
_ = global.EventBus.Unsubscribe(events.PlayerPropertyDurationUpdate, "sysmediacontrol.linux.duration")
|
||||
_ = global.EventBus.Unsubscribe(events.PlayerPropertyTimePosUpdate, "sysmediacontrol.linux.timepos")
|
||||
_ = global.EventBus.Unsubscribe(events.PlayerPropertyVolumeUpdate, "sysmediacontrol.linux.volume")
|
||||
_ = global.EventBus.Unsubscribe(events.PlayerPropertyStateUpdate, "sysmediacontrol.linux.state")
|
||||
|
||||
_, _ = linuxSMC.conn.ReleaseName(mprisBusName)
|
||||
_ = linuxSMC.conn.Close()
|
||||
linuxSMC = nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user