update wshub

This commit is contained in:
aynakeya
2024-04-29 20:22:35 -07:00
parent d514f96c28
commit 88066bd3b9
9 changed files with 489 additions and 19 deletions

View File

@@ -484,53 +484,53 @@
"en": "Text Output",
"zh-CN": "文本输出"
},
"plugin.webinfo.autostart": {
"plugin.wshub.autostart": {
"en": "Auto start",
"zh-CN": "自动启用"
},
"plugin.webinfo.description": {
"en": "Web output configuration",
"zh-CN": "web输出设置"
"plugin.wshub.description": {
"en": "Websocket Hub Configuration",
"zh-CN": "Websocket服务器设置"
},
"plugin.webinfo.port": {
"plugin.wshub.port": {
"en": "Port",
"zh-CN": "服务器端口"
},
"plugin.webinfo.server_control": {
"plugin.wshub.server_control": {
"en": "Control",
"zh-CN": "操作"
},
"plugin.webinfo.server_control.restart": {
"plugin.wshub.server_control.restart": {
"en": "Restart",
"zh-CN": "重启"
},
"plugin.webinfo.server_control.start": {
"plugin.wshub.server_control.start": {
"en": "Start",
"zh-CN": "启动"
},
"plugin.webinfo.server_control.stop": {
"plugin.wshub.server_control.stop": {
"en": "Stop",
"zh-CN": "停止"
},
"plugin.webinfo.server_preview": {
"en": "Server Preview",
"zh-CN": "效果预览"
"plugin.wshub.server_link": {
"en": "Websocket Server Link",
"zh-CN": "Websocket服务器链接"
},
"plugin.webinfo.server_status": {
"plugin.wshub.server_status": {
"en": "Server Status",
"zh-CN": "服务器状态"
},
"plugin.webinfo.server_status.running": {
"plugin.wshub.server_status.running": {
"en": "Running",
"zh-CN": "运行中"
},
"plugin.webinfo.server_status.stopped": {
"plugin.wshub.server_status.stopped": {
"en": "Stopped",
"zh-CN": "已停止"
},
"plugin.webinfo.title": {
"en": "Web Output",
"zh-CN": "Web输出"
"plugin.wshub.title": {
"en": "Websocket Hub",
"zh-CN": "Websocket服务器"
}
}
}

77
core/events/mapping.go Normal file
View File

@@ -0,0 +1,77 @@
package events
import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/pkg/event"
"encoding/json"
"errors"
"reflect"
)
var EventsMapping = map[event.EventId]any{
LiveRoomAddCmd: LiveRoomAddCmdEvent{},
LiveRoomProviderUpdate: LiveRoomProviderUpdateEvent{},
LiveRoomRemoveCmd: LiveRoomRemoveCmdEvent{},
LiveRoomRoomsUpdate: LiveRoomRoomsUpdateEvent{},
LiveRoomStatusUpdate: LiveRoomStatusUpdateEvent{},
LiveRoomConfigChangeCmd: LiveRoomConfigChangeCmdEvent{},
LiveRoomOperationCmd: LiveRoomOperationCmdEvent{},
PlayerVolumeChangeCmd: PlayerVolumeChangeCmdEvent{},
PlayerPlayCmd: PlayerPlayCmdEvent{},
PlayerPlayErrorUpdate: PlayerPlayErrorUpdateEvent{},
PlayerSeekCmd: PlayerSeekCmdEvent{},
PlayerToggleCmd: PlayerToggleCmdEvent{},
PlayerPlayNextCmd: PlayerPlayNextCmdEvent{},
PlayerLyricRequestCmd: PlayerLyricRequestCmdEvent{},
PlayerLyricReload: PlayerLyricReloadEvent{},
PlayerLyricPosUpdate: PlayerLyricPosUpdateEvent{},
PlayerPlayingUpdate: PlayerPlayingUpdateEvent{},
PlayerPropertyPauseUpdate: PlayerPropertyPauseUpdateEvent{},
PlayerPropertyPercentPosUpdate: PlayerPropertyPercentPosUpdateEvent{},
PlayerPropertyIdleActiveUpdate: PlayerPropertyIdleActiveUpdateEvent{},
PlayerPropertyTimePosUpdate: PlayerPropertyTimePosUpdateEvent{},
PlayerPropertyDurationUpdate: PlayerPropertyDurationUpdateEvent{},
PlayerPropertyVolumeUpdate: PlayerPropertyVolumeUpdateEvent{},
PlayerVideoPlayerSetWindowHandleCmd: PlayerVideoPlayerSetWindowHandleCmdEvent{},
PlayerSetAudioDeviceCmd: PlayerSetAudioDeviceCmdEvent{},
PlayerAudioDeviceUpdate: PlayerAudioDeviceUpdateEvent{},
PlaylistManagerSetSystemCmd: PlaylistManagerSetSystemCmdEvent{},
PlaylistManagerSystemUpdate: PlaylistManagerSystemUpdateEvent{},
PlaylistManagerRefreshCurrentCmd: PlaylistManagerRefreshCurrentCmdEvent{},
PlaylistManagerGetCurrentCmd: PlaylistManagerGetCurrentCmdEvent{},
PlaylistManagerCurrentUpdate: PlaylistManagerCurrentUpdateEvent{},
PlaylistManagerInfoUpdate: PlaylistManagerInfoUpdateEvent{},
PlaylistManagerAddPlaylistCmd: PlaylistManagerAddPlaylistCmdEvent{},
PlaylistManagerRemovePlaylistCmd: PlaylistManagerRemovePlaylistCmdEvent{},
MediaProviderUpdate: MediaProviderUpdateEvent{},
SearchCmd: SearchCmdEvent{},
SearchResultUpdate: SearchResultUpdateEvent{},
}
func init() {
for _, v := range []model.PlaylistID{model.PlaylistIDSystem, model.PlaylistIDPlayer, model.PlaylistIDHistory} {
EventsMapping[PlaylistDetailUpdate(v)] = PlaylistDetailUpdateEvent{}
EventsMapping[PlaylistMoveCmd(v)] = PlaylistMoveCmdEvent{}
EventsMapping[PlaylistSetIndexCmd(v)] = PlaylistSetIndexCmdEvent{}
EventsMapping[PlaylistDeleteCmd(v)] = PlaylistDeleteCmdEvent{}
EventsMapping[PlaylistInsertCmd(v)] = PlaylistInsertCmdEvent{}
EventsMapping[PlaylistInsertUpdate(v)] = PlaylistInsertUpdateEvent{}
EventsMapping[PlaylistNextCmd(v)] = PlaylistNextCmdEvent{}
EventsMapping[PlaylistNextUpdate(v)] = PlaylistNextUpdateEvent{}
EventsMapping[PlaylistModeChangeCmd(v)] = PlaylistModeChangeCmdEvent{}
EventsMapping[PlaylistModeChangeUpdate(v)] = PlaylistModeChangeUpdateEvent{}
}
}
func UnmarshalEventData(eventId event.EventId, data []byte) (any, error) {
val, ok := EventsMapping[eventId]
if !ok {
return nil, errors.New("event id not found")
}
newVal := reflect.New(reflect.TypeOf(val))
err := json.Unmarshal(data, newVal.Interface())
if err != nil {
return nil, err
}
return newVal.Elem().Interface(), nil
}

View File

@@ -0,0 +1,22 @@
package events
import (
"encoding/json"
"github.com/stretchr/testify/require"
"testing"
)
func TestUnmarshalEventData(t *testing.T) {
eventData := LiveRoomAddCmdEvent{
Title: "test",
Provider: "asdfasd",
RoomKey: "asdfasdf",
}
data, err := json.Marshal(eventData)
require.NoError(t, err)
val, err := UnmarshalEventData(LiveRoomAddCmd, data)
require.NoError(t, err)
resultData, ok := val.(LiveRoomAddCmdEvent)
require.True(t, ok)
require.Equal(t, eventData, resultData)
}

View File

@@ -12,6 +12,7 @@ import (
"AynaLivePlayer/plugin/qiege"
"AynaLivePlayer/plugin/sourcelogin"
"AynaLivePlayer/plugin/textinfo"
"AynaLivePlayer/plugin/wshub"
)
func Initialize() {
@@ -25,6 +26,7 @@ func Initialize() {
diange.NewDiange(), qiege.NewQiege(), sourcelogin.NewSourceLogin(),
textinfo.NewTextInfo(),
durationmgmt.NewMaxDuration(),
wshub.NewWsHub(),
)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/AynaLivePlayer/miaosic"
"github.com/aynakeya/go-mpv"
"github.com/tidwall/gjson"
"math"
)
var running bool = false
@@ -71,6 +72,9 @@ func StopPlayer() {
log.Info("mpv player stopped")
}
var prevPercentPos float64 = 0
var prevTimePos float64 = 0
var mpvPropertyHandler = map[string]func(value any){
"pause": func(value any) {
var data events.PlayerPropertyPauseUpdateEvent
@@ -89,6 +93,11 @@ var mpvPropertyHandler = map[string]func(value any){
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)
},
@@ -120,6 +129,11 @@ var mpvPropertyHandler = map[string]func(value any){
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) {

View File

@@ -1,7 +1,6 @@
package webinfo
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/pkg/config"
"context"
"encoding/json"

16
plugin/wshub/events.go Normal file
View File

@@ -0,0 +1,16 @@
package wshub
import (
"AynaLivePlayer/pkg/event"
"encoding/json"
)
type EventData struct {
EventID event.EventId
Data interface{}
}
type EventDataReceived struct {
EventID event.EventId
Data json.RawMessage
}

165
plugin/wshub/server.go Normal file
View File

@@ -0,0 +1,165 @@
package wshub
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/logger"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/websocket"
"net/http"
"sync"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type wsClient struct {
conn *websocket.Conn
Data chan []byte
Close chan byte
}
func (c *wsClient) start() {
for {
msgType, val, err := c.conn.ReadMessage()
if err != nil {
c.Close <- 1
return
}
if msgType == websocket.TextMessage {
return
}
var data EventDataReceived
err = json.Unmarshal(val, &data)
if err != nil {
global.Logger.Warn("unmarshal event data failed", err)
return
}
actualEventData, err := events.UnmarshalEventData(data.EventID, data.Data)
if err != nil {
global.Logger.Warn("unmarshal event data failed", err)
return
}
global.EventManager.CallA(data.EventID, actualEventData)
}
}
type wsServer struct {
Running bool
Server *http.Server
clients map[*wsClient]bool
mux *http.ServeMux
lock sync.RWMutex
port *int
log logger.ILogger
}
func newWsServer(port *int) *wsServer {
mux := http.NewServeMux()
s := &wsServer{
Running: false,
clients: make(map[*wsClient]bool),
mux: mux,
port: port,
log: global.Logger.WithPrefix("plugin.wshub.server"),
}
mux.HandleFunc("/wsinfo", s.handleWsInfo)
return s
}
func (s *wsServer) broadcast(data []byte) {
s.lock.RLock()
defer s.lock.RUnlock()
for client := range s.clients {
client.Data <- data
}
}
func (s *wsServer) register(client *wsClient) {
s.lock.Lock()
s.clients[client] = true
s.lock.Unlock()
}
func (s *wsServer) unregister(client *wsClient) {
s.lock.Lock()
delete(s.clients, client)
s.lock.Unlock()
}
func (s *wsServer) handleWsInfo(w http.ResponseWriter, r *http.Request) {
s.log.Debug("connection start")
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
s.log.Warnf("upgrade error: %s", err)
return
}
client := &wsClient{
conn: conn,
Data: make(chan []byte, 16),
Close: make(chan byte, 1),
}
s.register(client)
defer s.unregister(client)
go client.start()
for {
select {
case data := <-client.Data:
err := client.conn.WriteMessage(websocket.TextMessage, data)
if err != nil {
s.log.Warn("write message failed", err)
return
}
case _ = <-client.Close:
s.log.Infof("client %s close", client.conn.RemoteAddr().String())
if err := client.conn.Close(); err != nil {
s.log.Warnf("close connection encouter an error: %s", err)
}
return
}
}
}
func (s *wsServer) Start() {
s.log.Debug("WebInfoServer starting...")
s.Running = true
go func() {
s.Server = &http.Server{
Addr: fmt.Sprintf("localhost:%d", *s.port),
Handler: s.mux,
}
err := s.Server.ListenAndServe()
s.Running = false
if errors.Is(err, http.ErrServerClosed) {
s.log.Info("WebInfoServer closed")
return
}
if err != nil {
s.log.Errorf("Failed to start webinfo server: %s", err)
return
}
}()
}
func (s *wsServer) Stop() error {
s.log.Debug("WebInfoServer stopping...")
s.lock.Lock()
s.clients = make(map[*wsClient]bool)
s.lock.Unlock()
if s.Server != nil {
return s.Server.Shutdown(context.TODO())
}
return nil
}
func (s *wsServer) getWsUrl() string {
return fmt.Sprintf("ws://localhost:%d/wsinfo", *s.port)
}

175
plugin/wshub/wshub.go Normal file
View File

@@ -0,0 +1,175 @@
package wshub
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/gui"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"AynaLivePlayer/pkg/logger"
"encoding/json"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type WsHub struct {
config.BaseConfig
Enabled bool
Port int
panel fyne.CanvasObject
server *wsServer
log logger.ILogger
}
func NewWsHub() *WsHub {
return &WsHub{
Enabled: false,
Port: 29629,
log: global.Logger.WithPrefix("plugin.wshub"),
}
}
func (w *WsHub) Enable() error {
config.LoadConfig(w)
w.server = newWsServer(&w.Port)
gui.AddConfigLayout(w)
w.registerEvents()
w.log.Info("webinfo loaded")
if w.Enabled {
w.log.Info("starting web backend server")
w.server.Start()
}
return nil
}
func (w *WsHub) Disable() error {
if w.server.Running {
err := w.server.Stop()
if err != nil {
w.log.Warnf("stop server have error: %s", err)
}
}
return nil
}
func (w *WsHub) Name() string {
return "WsHub"
}
func (w *WsHub) Title() string {
return i18n.T("plugin.wshub.title")
}
func (w *WsHub) Description() string {
return i18n.T("plugin.wshub.description")
}
func (w *WsHub) CreatePanel() fyne.CanvasObject {
if w.panel != nil {
return w.panel
}
statusText := widget.NewLabel("")
freshStatusText := func() {
if w.server.Running {
statusText.SetText(i18n.T("plugin.wshub.server_status.running"))
return
} else {
statusText.SetText(i18n.T("plugin.wshub.server_status.stopped"))
}
}
serverStatus := container.NewHBox(
widget.NewLabel(i18n.T("plugin.wshub.server_status")),
statusText,
)
autoStart := container.NewHBox(
widget.NewLabel(i18n.T("plugin.wshub.autostart")),
component.NewCheckOneWayBinding("", &w.Enabled, w.Enabled))
freshStatusText()
serverPort := container.NewBorder(nil, nil,
widget.NewLabel(i18n.T("plugin.wshub.port")), nil,
widget.NewEntryWithData(binding.IntToString(binding.BindInt(&w.Port))),
)
serverUrl := widget.NewEntry()
serverUrl.SetText(w.server.getWsUrl())
serverUrl.Disable()
serverPreview := container.NewBorder(nil, nil,
widget.NewLabel(i18n.T("plugin.wshub.server_link")), nil,
serverUrl,
)
refreshServerUrl := func() {
serverUrl.SetText(w.server.getWsUrl())
}
stopBtn := component.NewAsyncButtonWithIcon(
i18n.T("plugin.wshub.server_control.stop"),
theme.MediaStopIcon(),
func() {
if !w.server.Running {
return
}
w.log.Info("User try stop webinfo server")
err := w.server.Stop()
if err != nil {
w.log.Warnf("stop server have error: %s", err)
}
freshStatusText()
},
)
startBtn := component.NewAsyncButtonWithIcon(
i18n.T("plugin.wshub.server_control.start"),
theme.MediaPlayIcon(),
func() {
if w.server.Running {
return
}
w.log.Infof("User try start webinfo server with port %d", w.Port)
w.server.Start()
freshStatusText()
refreshServerUrl()
},
)
restartBtn := component.NewAsyncButtonWithIcon(
i18n.T("plugin.wshub.server_control.restart"),
theme.MediaReplayIcon(),
func() {
w.log.Infof("User try restart webinfo server with port %d", w.Port)
if w.server.Running {
if err := w.server.Stop(); err != nil {
w.log.Warnf("stop server have error: %s", err)
return
}
}
w.server.Start()
freshStatusText()
refreshServerUrl()
},
)
ctrlBtns := container.NewHBox(
widget.NewLabel(i18n.T("plugin.wshub.server_control")),
startBtn, stopBtn, restartBtn,
)
w.panel = container.NewVBox(serverStatus, autoStart, serverPreview, serverPort, ctrlBtns)
return nil
}
func (w *WsHub) registerEvents() {
for eid, _ := range events.EventsMapping {
global.EventManager.RegisterA(eid,
"plugin.wshub.event."+string(eid),
func(e *event.Event) {
val, err := json.Marshal(EventData{
EventID: e.Id,
Data: e.Data,
})
if err != nil {
w.log.Errorf("failed to marshal event data %v", err)
return
}
w.server.broadcast(val)
})
}
}