Merge pull request #39 from AynaLivePlayer/dev

1.2.0
This commit is contained in:
Aynakeya
2025-07-02 23:39:49 +08:00
committed by GitHub
25 changed files with 660 additions and 108 deletions

74
go.mod
View File

@@ -1,36 +1,39 @@
module AynaLivePlayer
go 1.19
go 1.23.0
toolchain go1.24.4
replace (
github.com/AynaLivePlayer/liveroom-sdk v0.1.0 => ./pkg/liveroom-sdk // submodule
github.com/AynaLivePlayer/miaosic v0.1.5 => ./pkg/miaosic // submodule
github.com/AynaLivePlayer/miaosic v0.1.7 => ./pkg/miaosic // submodule
github.com/saltosystems/winrt-go => github.com/go-musicfox/winrt-go v0.1.4 // winrt with media foundation
)
require (
fyne.io/fyne/v2 v2.5.4
fyne.io/fyne/v2 v2.6.1
github.com/AynaLivePlayer/liveroom-sdk v0.1.0
github.com/AynaLivePlayer/miaosic v0.1.5
github.com/AynaLivePlayer/miaosic v0.1.7
github.com/adrg/libvlc-go/v3 v3.1.6
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/aynakeya/go-mpv v0.0.6
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a
github.com/aynakeya/go-mpv v0.0.7
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.7.0
github.com/go-resty/resty/v2 v2.16.5
github.com/gorilla/websocket v1.5.3
github.com/mattn/go-colorable v0.1.12
github.com/mattn/go-colorable v0.1.14
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/saltosystems/winrt-go v0.0.0-20240320184339-289d313a74b7
github.com/saltosystems/winrt-go v0.0.0-20241223121953-98e32661f6ff
github.com/sirupsen/logrus v1.9.3
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stretchr/testify v1.8.4
github.com/tidwall/gjson v1.17.3
github.com/stretchr/testify v1.10.0
github.com/tidwall/gjson v1.18.0
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
go.uber.org/zap v1.26.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/sys v0.25.0
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
golang.org/x/sys v0.33.0
gopkg.in/ini.v1 v1.67.0
)
@@ -38,45 +41,46 @@ require (
fyne.io/systray v1.11.0 // indirect
github.com/AynaLivePlayer/blivedm-go v0.0.0-20250527143915-74cc4b2603bc // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/PuerkitoBio/goquery v1.7.1 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/XiaoMengXinX/Music163Api-Go v0.1.30 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/cascadia v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aynakeya/deepcolor v1.0.3 // indirect
github.com/aynakeya/open-bilibili-live v0.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 // indirect
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect
github.com/fyne-io/glfw-js v0.0.0-20241126112943-313d8a0fe1d0 // indirect
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
github.com/fyne-io/gl-js v0.1.0 // indirect
github.com/fyne-io/glfw-js v0.2.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.1.0 // indirect
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.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/gopherjs/gopherjs v1.17.2 // indirect
github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.3.0 // indirect
github.com/sahilm/fuzzy v0.1.0 // indirect
github.com/rymdport/portal v0.4.1 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/yuin/goldmark v1.7.1 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -37,7 +37,8 @@ func (b *AsyncButton) SetOnTapped(f func()) {
b.Disable()
go func() {
f()
b.Enable()
//time.Sleep(3 * time.Second)
fyne.Do(b.Enable)
}()
}
}

View File

@@ -5,6 +5,7 @@ import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
@@ -44,10 +45,10 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
})
global.EventManager.RegisterA(events.PlaylistModeChangeUpdate(model.PlaylistIDPlayer),
"gui.config.basic.random_playlist.player",
func(event *event.Event) {
gutil.ThreadSafeHandler(func(event *event.Event) {
data := event.Data.(events.PlaylistModeChangeUpdateEvent)
playerRandomCheck.SetChecked(data.Mode == model.PlaylistModeRandom)
})
}))
systemRandomCheck := widget.NewCheck(i18n.T("gui.config.basic.random_playlist.system"),
func(b bool) {
@@ -63,10 +64,10 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
global.EventManager.RegisterA(events.PlaylistModeChangeUpdate(model.PlaylistIDSystem),
"gui.config.basic.random_playlist.system",
func(event *event.Event) {
gutil.ThreadSafeHandler(func(event *event.Event) {
data := event.Data.(events.PlaylistModeChangeUpdateEvent)
systemRandomCheck.SetChecked(data.Mode == model.PlaylistModeRandom)
})
}))
randomPlaylist := container.NewHBox(
widget.NewLabel(i18n.T("gui.config.basic.random_playlist")),
@@ -86,7 +87,7 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
global.EventManager.RegisterA(
events.PlayerAudioDeviceUpdate,
"gui.config.basic.audio_device.update",
func(event *event.Event) {
gutil.ThreadSafeHandler(func(event *event.Event) {
data := event.Data.(events.PlayerAudioDeviceUpdateEvent)
devices := make([]string, len(data.Devices))
deviceDesc2Name = make(map[string]string)
@@ -102,7 +103,7 @@ func (b *bascicConfig) CreatePanel() fyne.CanvasObject {
deviceSel.Options = devices
deviceSel.Selected = currentDevice
deviceSel.Refresh()
})
}))
outputDevice := container.NewBorder(nil, nil,
widget.NewLabel(i18n.T("gui.config.basic.audio_device")), nil,

View File

@@ -4,6 +4,7 @@ import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
@@ -14,6 +15,7 @@ import (
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
"os"
_logger "AynaLivePlayer/pkg/logger"
)
@@ -33,7 +35,9 @@ func Initialize() {
logger = global.Logger.WithPrefix("GUI")
black_magic()
logger.Info("Initializing GUI")
//os.Setenv("FYNE_FONT", config.GetAssetPath("msyh.ttc"))
if config.General.CustomFonts != "" {
_ = os.Setenv("FYNE_FONT", config.GetAssetPath(config.General.CustomFonts))
}
App = app.NewWithID(config.ProgramName)
//App.Settings().SetTheme(&myTheme{})
MainWindow = App.NewWindow(fmt.Sprintf("%s Ver %s", config.ProgramName, model.Version(config.Version)))
@@ -65,18 +69,20 @@ func Initialize() {
//MainWindow.Resize(fyne.NewSize(1280, 720))
MainWindow.Resize(fyne.NewSize(config.General.Width, config.General.Height))
setupPlayerWindow()
// todo: fix, window were created even if not show. this block gui from closing
// i can't create sub window before the main window shows.
// setupPlayerWindow()
// register error
global.EventManager.RegisterA(
events.ErrorUpdate, "gui.show_error", func(e *event.Event) {
events.ErrorUpdate, "gui.show_error", gutil.ThreadSafeHandler(func(e *event.Event) {
err := e.Data.(events.ErrorUpdateEvent).Error
logger.Warnf("gui received error event: %v, %v", err, err == nil)
if err == nil {
return
}
dialog.ShowError(err, MainWindow)
})
}))
checkUpdate()
MainWindow.SetFixedSize(config.General.FixedSize)
@@ -91,7 +97,9 @@ func Initialize() {
})
}
MainWindow.SetOnClosed(func() {
logger.Infof("GUI closing")
if playerWindow != nil {
logger.Infof("player window closing")
playerWindow.Close()
}
})

15
gui/gutil/fyne.go Normal file
View File

@@ -0,0 +1,15 @@
package gutil
import (
"AynaLivePlayer/pkg/event"
"fyne.io/fyne/v2"
)
// since 2.6.1, calls to fyne API from other go routine must be wrapped in fyne.Do
func ThreadSafeHandler(fn func(e *event.Event)) func(e *event.Event) {
return func(e *event.Event) {
fyne.Do(func() {
fn(e)
})
}
}

View File

@@ -4,6 +4,7 @@ import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fmt"
@@ -76,10 +77,10 @@ func createHistoryList() fyne.CanvasObject {
func registerHistoryHandler() {
global.EventManager.RegisterA(
events.PlaylistDetailUpdate(model.PlaylistIDHistory),
"gui.history.update", func(event *event.Event) {
"gui.history.update", gutil.ThreadSafeHandler(func(event *event.Event) {
History.mux.Lock()
History.Medias = event.Data.(events.PlaylistDetailUpdateEvent).Medias
History.List.Refresh()
History.mux.Unlock()
})
}))
}

View File

@@ -4,6 +4,7 @@ import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/gui/xfyne"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
@@ -135,14 +136,14 @@ func registerRoomHandlers() {
global.EventManager.RegisterA(
events.LiveRoomProviderUpdate,
"gui.liveroom.provider_update",
func(event *event.Event) {
gutil.ThreadSafeHandler(func(event *event.Event) {
RoomTab.providers = event.Data.(events.LiveRoomProviderUpdateEvent).Providers
RoomTab.Rooms.Refresh()
})
}))
global.EventManager.RegisterA(
events.LiveRoomRoomsUpdate,
"gui.liveroom.rooms_update",
func(event *event.Event) {
gutil.ThreadSafeHandler(func(event *event.Event) {
logger.Infof("Update rooms")
data := event.Data.(events.LiveRoomRoomsUpdateEvent)
RoomTab.lock.Lock()
@@ -150,11 +151,11 @@ func registerRoomHandlers() {
RoomTab.Rooms.Select(0)
RoomTab.Rooms.Refresh()
RoomTab.lock.Unlock()
})
}))
global.EventManager.RegisterA(
events.LiveRoomStatusUpdate,
"gui.liveroom.room_status_update",
func(event *event.Event) {
gutil.ThreadSafeHandler(func(event *event.Event) {
room := event.Data.(events.LiveRoomStatusUpdateEvent).Room
index := -1
for i := 0; i < len(RoomTab.rooms); i++ {
@@ -182,7 +183,7 @@ func registerRoomHandlers() {
}
RoomTab.Status.Refresh()
}
})
}))
}
@@ -216,10 +217,10 @@ func createRoomController() fyne.CanvasObject {
global.EventManager.RegisterA(
events.LiveRoomOperationFinish,
"gui.liveroom.operation_finish",
func(event *event.Event) {
gutil.ThreadSafeHandler(func(event *event.Event) {
RoomTab.ConnectBtn.Enable()
RoomTab.DisConnectBtn.Enable()
})
}))
RoomTab.Status = widget.NewLabel(i18n.T("gui.room.waiting"))
RoomTab.RoomTitle = widget.NewLabel("")
RoomTab.RoomID = widget.NewLabel("")

View File

@@ -59,7 +59,7 @@ func registerPlayControllerHandler() {
PlayController.ButtonLrc.OnTapped = func() {
if !PlayController.LrcWindowOpen {
PlayController.LrcWindowOpen = true
createLyricWindow().Show()
createLyricWindow().Close()
}
}
@@ -67,26 +67,25 @@ func registerPlayControllerHandler() {
showPlayerWindow()
}
global.EventManager.RegisterA(events.PlayerPropertyPauseUpdate, "gui.player.controller.paused", func(event *event.Event) {
global.EventManager.RegisterA(events.PlayerPropertyPauseUpdate, "gui.player.controller.paused", gutil.ThreadSafeHandler(func(event *event.Event) {
if event.Data.(events.PlayerPropertyPauseUpdateEvent).Paused {
PlayController.ButtonSwitch.Icon = theme.MediaPlayIcon()
} else {
PlayController.ButtonSwitch.Icon = theme.MediaPauseIcon()
}
PlayController.ButtonSwitch.Refresh()
})
}))
global.EventManager.RegisterA(events.PlayerPropertyPercentPosUpdate, "gui.player.controller.percent_pos", func(event *event.Event) {
global.EventManager.RegisterA(events.PlayerPropertyPercentPosUpdate, "gui.player.controller.percent_pos", gutil.ThreadSafeHandler(func(event *event.Event) {
if PlayController.Progress.Dragging {
return
}
PlayController.Progress.Value = event.Data.(events.PlayerPropertyPercentPosUpdateEvent).PercentPos * 10
PlayController.Progress.Refresh()
})
}))
global.EventManager.RegisterA(events.PlayerPropertyIdleActiveUpdate, "gui.player.controller.idle_active", func(event *event.Event) {
global.EventManager.RegisterA(events.PlayerPropertyIdleActiveUpdate, "gui.player.controller.idle_active", gutil.ThreadSafeHandler(func(event *event.Event) {
isIdle := event.Data.(events.PlayerPropertyIdleActiveUpdateEvent).IsIdle
// todo: @3
if isIdle {
PlayController.Progress.Value = 0
PlayController.Progress.Max = 0
@@ -97,7 +96,7 @@ func registerPlayControllerHandler() {
} else {
PlayController.Progress.Max = 1000
}
})
}))
PlayController.Progress.Max = 0
PlayController.Progress.OnDragEnd = func(f float64) {
@@ -107,18 +106,18 @@ func registerPlayControllerHandler() {
})
}
global.EventManager.RegisterA(events.PlayerPropertyTimePosUpdate, "gui.player.controller.time_pos", func(event *event.Event) {
global.EventManager.RegisterA(events.PlayerPropertyTimePosUpdate, "gui.player.controller.time_pos", gutil.ThreadSafeHandler(func(event *event.Event) {
PlayController.CurrentTime.SetText(util.FormatTime(int(event.Data.(events.PlayerPropertyTimePosUpdateEvent).TimePos)))
})
}))
global.EventManager.RegisterA(events.PlayerPropertyDurationUpdate, "gui.player.controller.duration", func(event *event.Event) {
global.EventManager.RegisterA(events.PlayerPropertyDurationUpdate, "gui.player.controller.duration", gutil.ThreadSafeHandler(func(event *event.Event) {
PlayController.TotalTime.SetText(util.FormatTime(int(event.Data.(events.PlayerPropertyDurationUpdateEvent).Duration)))
})
}))
global.EventManager.RegisterA(events.PlayerPropertyVolumeUpdate, "gui.player.controller.volume", func(event *event.Event) {
global.EventManager.RegisterA(events.PlayerPropertyVolumeUpdate, "gui.player.controller.volume", gutil.ThreadSafeHandler(func(event *event.Event) {
PlayController.Volume.Value = event.Data.(events.PlayerPropertyVolumeUpdateEvent).Volume
PlayController.Volume.Refresh()
})
}))
PlayController.Volume.OnChanged = func(f float64) {
global.EventManager.CallA(events.PlayerVolumeChangeCmd, events.PlayerVolumeChangeCmdEvent{
@@ -126,7 +125,8 @@ func registerPlayControllerHandler() {
})
}
global.EventManager.RegisterA(events.PlayerPlayingUpdate, "gui.player.updateinfo", func(event *event.Event) {
// todo: double check cover loading for new thread model
global.EventManager.RegisterA(events.PlayerPlayingUpdate, "gui.player.updateinfo", gutil.ThreadSafeHandler(func(event *event.Event) {
if event.Data.(events.PlayerPlayingUpdateEvent).Removed {
PlayController.Progress.Value = 0
PlayController.Progress.Max = 0
@@ -176,12 +176,12 @@ func registerPlayControllerHandler() {
return
}
PlayController.Cover.Resource = pic.Resource
PlayController.Cover.Refresh()
fyne.Do(PlayController.Cover.Refresh)
}
}()
}
})
}))
}
func createPlayControllerV2() fyne.CanvasObject {

View File

@@ -3,6 +3,7 @@ package gui
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fyne.io/fyne/v2"
@@ -41,8 +42,9 @@ func createLyricWindow() fyne.Window {
w.CenterOnScreen()
// register handlers
// todo: lyric not update correctly, known bug https://github.com/fyne-io/fyne/pull/5783
global.EventManager.RegisterA(
events.PlayerLyricPosUpdate, "player.lyric.current_lyric", func(event *event.Event) {
events.PlayerLyricPosUpdate, "player.lyric.current_lyric", gutil.ThreadSafeHandler(func(event *event.Event) {
e := event.Data.(events.PlayerLyricPosUpdateEvent)
logger.Debug("lyric update", e)
if prevIndex >= len(fullLrc.Objects) || e.CurrentIndex >= len(fullLrc.Objects) {
@@ -66,13 +68,13 @@ func createLyricWindow() fyne.Window {
},
})
fullLrc.Refresh()
})
}))
global.EventManager.RegisterA(events.PlayerLyricReload, "player.lyric.current_lyric", func(event *event.Event) {
global.EventManager.RegisterA(events.PlayerLyricReload, "player.lyric.current_lyric", gutil.ThreadSafeHandler(func(event *event.Event) {
e := event.Data.(events.PlayerLyricReloadEvent)
fullLrc.Objects = createLyricObj(&e.Lyrics)
lrcWindow.Refresh()
})
}))
global.EventManager.CallA(events.PlayerLyricRequestCmd, events.PlayerLyricRequestCmdEvent{})

View File

@@ -4,6 +4,7 @@ import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fmt"
@@ -74,12 +75,12 @@ func createPlaylist() fyne.CanvasObject {
object.(*fyne.Container).Objects[1].(*widget.Label).SetText(fmt.Sprintf("%d", id))
object.(*fyne.Container).Objects[2].(*playlistOperationButton).Index = id
})
global.EventManager.RegisterA(events.PlaylistDetailUpdate(model.PlaylistIDPlayer), "gui.player.playlist.update", func(event *event.Event) {
global.EventManager.RegisterA(events.PlaylistDetailUpdate(model.PlaylistIDPlayer), "gui.player.playlist.update", gutil.ThreadSafeHandler(func(event *event.Event) {
UserPlaylist.mux.Lock()
UserPlaylist.Medias = event.Data.(events.PlaylistDetailUpdateEvent).Medias
UserPlaylist.List.Refresh()
UserPlaylist.mux.Unlock()
})
}))
return container.NewBorder(
container.NewBorder(nil, nil,
widget.NewLabel("#"), widget.NewLabel(i18n.T("gui.player.playlist.ops")),

View File

@@ -17,6 +17,9 @@ func setupPlayerWindow() {
}
func showPlayerWindow() {
if playerWindow == nil {
setupPlayerWindow()
}
playerWindow.Show()
if playerWindowHandle == 0 {
playerWindowHandle = xfyne.GetWindowHandle(playerWindow)

View File

@@ -4,6 +4,7 @@ import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/gui/xfyne"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
@@ -97,14 +98,14 @@ func createPlaylists() fyne.CanvasObject {
})
}
global.EventManager.RegisterA(events.MediaProviderUpdate,
"gui.playlists.provider.update", func(event *event.Event) {
"gui.playlists.provider.update", gutil.ThreadSafeHandler(func(event *event.Event) {
providers := event.Data.(events.MediaProviderUpdateEvent)
s := make([]string, len(providers.Providers))
copy(s, providers.Providers)
PlaylistManager.providers = s
})
}))
global.EventManager.RegisterA(events.PlaylistManagerInfoUpdate,
"gui.playlists.info.update", func(event *event.Event) {
"gui.playlists.info.update", gutil.ThreadSafeHandler(func(event *event.Event) {
data := event.Data.(events.PlaylistManagerInfoUpdateEvent)
prevLen := len(PlaylistManager.currentPlaylists)
PlaylistManager.currentPlaylists = data.Playlists
@@ -113,12 +114,12 @@ func createPlaylists() fyne.CanvasObject {
if prevLen != len(PlaylistManager.currentPlaylists) {
PlaylistManager.Playlists.Select(0)
}
})
}))
global.EventManager.RegisterA(events.PlaylistManagerSystemUpdate,
"gui.playlists.system.update", func(event *event.Event) {
"gui.playlists.system.update", gutil.ThreadSafeHandler(func(event *event.Event) {
data := event.Data.(events.PlaylistManagerSystemUpdateEvent)
PlaylistManager.CurrentSystemPlaylist.SetText(i18n.T("gui.playlist.current") + data.Info.DisplayName())
})
}))
return container.NewHBox(
container.NewBorder(
nil, container.NewCenter(container.NewHBox(PlaylistManager.AddBtn, PlaylistManager.RemoveBtn)),
@@ -190,12 +191,12 @@ func createPlaylistMedias() fyne.CanvasObject {
}
})
global.EventManager.RegisterA(events.PlaylistManagerCurrentUpdate,
"gui.playlists.current.update", func(event *event.Event) {
"gui.playlists.current.update", gutil.ThreadSafeHandler(func(event *event.Event) {
logger.Infof("receive current playlist update, try to refresh playlist medias")
data := event.Data.(events.PlaylistManagerCurrentUpdateEvent)
PlaylistManager.currentMedias = data.Medias
PlaylistManager.PlaylistMedia.Refresh()
})
}))
return container.NewBorder(
container.NewHBox(PlaylistManager.RefreshBtn, PlaylistManager.SetAsSystemBtn, PlaylistManager.CurrentSystemPlaylist), nil,
nil, nil,

View File

@@ -5,6 +5,7 @@ import (
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/component"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fyne.io/fyne/v2"
@@ -14,7 +15,7 @@ import (
var SearchBar = &struct {
Input *component.Entry
Button *component.AsyncButton
Button *widget.Button
UseSource *widget.Select
}{}
@@ -26,7 +27,7 @@ func createSearchBar() fyne.CanvasObject {
SearchBar.Button.OnTapped()
}
}
SearchBar.Button = component.NewAsyncButton(i18n.T("gui.search.search"), func() {
SearchBar.Button = widget.NewButton(i18n.T("gui.search.search"), func() {
keyword := SearchBar.Input.Text
pr := SearchBar.UseSource.Selected
logger.Debugf("Search keyword: %s, provider: %s", keyword, pr)
@@ -41,7 +42,7 @@ func createSearchBar() fyne.CanvasObject {
})
global.EventManager.RegisterA(events.MediaProviderUpdate,
"gui.search.provider.update", func(event *event.Event) {
"gui.search.provider.update", gutil.ThreadSafeHandler(func(event *event.Event) {
providers := event.Data.(events.MediaProviderUpdateEvent)
s := make([]string, len(providers.Providers))
copy(s, providers.Providers)
@@ -49,7 +50,7 @@ func createSearchBar() fyne.CanvasObject {
if len(s) > 0 {
SearchBar.UseSource.SetSelected(s[0])
}
})
}))
SearchBar.UseSource = widget.NewSelect([]string{}, func(s string) {
})

View File

@@ -4,6 +4,7 @@ import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fmt"
@@ -61,13 +62,13 @@ func createSearchList() fyne.CanvasObject {
})
}
})
global.EventManager.RegisterA(events.SearchResultUpdate, "gui.search.update_result", func(event *event.Event) {
global.EventManager.RegisterA(events.SearchResultUpdate, "gui.search.update_result", gutil.ThreadSafeHandler(func(event *event.Event) {
items := event.Data.(events.SearchResultUpdateEvent).Medias
SearchResult.Items = items
SearchResult.mux.Lock()
SearchResult.List.Refresh()
SearchResult.mux.Unlock()
})
}))
return container.NewBorder(
container.NewBorder(nil, nil,
widget.NewLabel("#"), widget.NewLabel(i18n.T("gui.search.operation")),

View File

@@ -3,6 +3,7 @@ package gui
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/gui/gutil"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/i18n"
"fyne.io/fyne/v2/dialog"
@@ -11,7 +12,7 @@ import (
func checkUpdate() {
global.EventManager.RegisterA(
events.CheckUpdateResultUpdate, "gui.updater.check_update", func(event *event.Event) {
events.CheckUpdateResultUpdate, "gui.updater.check_update", gutil.ThreadSafeHandler(func(event *event.Event) {
data := event.Data.(events.CheckUpdateResultUpdateEvent)
msg := data.Info.Version.String() + "\n\n\n" + data.Info.Info
if data.HasUpdate {
@@ -27,5 +28,5 @@ func checkUpdate() {
widget.NewRichTextFromMarkdown(""),
MainWindow)
}
})
}))
}

View File

@@ -11,6 +11,7 @@ import (
// getGlfwWindow returns the glfw.Window pointer from a fyne.Window.
// very unsafe and ugly hacks. but it works.
// todo: replace with LifeCycle https://github.com/fyne-io/fyne/issues/4483
func getGlfwWindow(window fyne.Window) *glfw.Window {
rv := reflect.ValueOf(window)
if rv.Type().String() != "*glfw.window" {

View File

@@ -1,11 +1,24 @@
package player
import "AynaLivePlayer/internal/player/mpv"
import (
"AynaLivePlayer/internal/player/mpv"
"AynaLivePlayer/internal/player/vlc"
"AynaLivePlayer/pkg/config"
)
func SetupMpvPlayer() {
if config.Experimental.PlayerCore == "vlc" {
vlc.SetupPlayer()
} else {
mpv.SetupPlayer()
}
}
func StopMpvPlayer() {
if config.Experimental.PlayerCore == "vlc" {
vlc.StopPlayer()
} else {
mpv.StopPlayer()
}
}

View File

@@ -0,0 +1,46 @@
package vlc
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/event"
)
type playerConfig struct {
Volume float64
AudioDevice string
DisplayMusicCover bool
}
func (p *playerConfig) Name() string {
return "Player"
}
func (p *playerConfig) OnLoad() {
return
}
func (p *playerConfig) OnSave() {
return
}
var cfg = &playerConfig{
Volume: 100,
DisplayMusicCover: true,
}
func restoreConfig() {
global.EventManager.CallA(events.PlayerVolumeChangeCmd, events.PlayerVolumeChangeCmdEvent{
Volume: cfg.Volume,
})
global.EventManager.RegisterA(events.PlayerPropertyVolumeUpdate, "player.config.volume", func(evnt *event.Event) {
data := evnt.Data.(events.PlayerPropertyVolumeUpdateEvent)
if data.Volume < 0 {
return
}
cfg.Volume = data.Volume
})
global.EventManager.CallA(events.PlayerSetAudioDeviceCmd, events.PlayerSetAudioDeviceCmdEvent{
Device: cfg.AudioDevice,
})
}

443
internal/player/vlc/vlc.go Normal file
View File

@@ -0,0 +1,443 @@
package vlc
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/event"
"AynaLivePlayer/pkg/logger"
"fmt"
"github.com/AynaLivePlayer/miaosic"
"github.com/adrg/libvlc-go/v3"
"math"
"runtime"
"strings"
"sync"
)
var running bool = false
var log logger.ILogger = nil
var player *vlc.Player
var eventManager *vlc.EventManager
var lock sync.Mutex
// 状态变量
var prevPercentPos float64 = 0
var prevTimePos float64 = 0
var duration float64 = 0
var currentMedia model.Media
var currentWindowHandle uintptr
var audioDevices []model.AudioDevice
var currentAudioDevice string
var videoOptions = map[string][]string{
"windows": {"--video-filter=adjust", "--directx-hwnd"},
"darwin": {"--vout=macosx"},
"linux": {"--vout=x11", "--x11-display=:0"},
}
func setWindowHandle(handle uintptr) error {
return nil
os := runtime.GOOS
switch os {
case "windows":
// Windows 平台使用 DirectX
player.SetHWND(uintptr(handle))
case "darwin":
// macOS 平台使用 NSView
player.SetNSObject(handle)
case "linux":
// Linux 平台使用 XWindow
player.SetXWindow(uint32(handle))
default:
return fmt.Errorf("unsupported platform: %s", os)
}
currentWindowHandle = handle
// 如果当前有媒体在播放,需要重新加载视频输出
if player.IsPlaying() {
player.Stop()
player.Play()
}
return nil
}
func SetupPlayer() {
running = true
config.LoadConfig(cfg)
log = global.Logger.WithPrefix("VLC Player")
opts := []string{"--no-video", "--quiet"}
//os := runtime.GOOS
//if platformOpts, ok := videoOptions[os]; ok {
// opts = append(opts, platformOpts...)
//}
// 初始化libvlc
if err := vlc.Init(opts...); err != nil {
log.Error("initialize libvlc failed: ", err)
return
}
// 创建播放器
var err error
player, err = vlc.NewPlayer()
if err != nil {
log.Error("create player failed: ", err)
return
}
// 获取事件管理器
eventManager, err = player.EventManager()
if err != nil {
log.Error("get event manager failed: ", err)
return
}
// 注册事件
registerEvents()
registerCmdHandler()
updateAudioDeviceList()
restoreConfig()
log.Info("VLC player initialized")
}
func StopPlayer() {
log.Info("stopping VLC player")
if currentAudioDevice != "" {
cfg.AudioDevice = currentAudioDevice
log.Infof("save audio device config: %s", cfg.AudioDevice)
}
running = false
if player != nil {
err := player.Stop()
if err != nil {
log.Error("stop player failed: ", err)
}
err = player.Release()
if err != nil {
log.Error("release player failed: ", err)
}
}
err := vlc.Release()
if err != nil {
log.Error("release player failed: ", err)
}
log.Info("VLC player stopped")
}
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.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: model.Media{},
Removed: true,
})
}, nil)
if err != nil {
log.Error("register MediaPlayerEndReached event failed: ", err)
}
// 播放位置改变事件
_, err = eventManager.Attach(vlc.MediaPlayerPositionChanged, func(e vlc.Event, userData interface{}) {
pos32, _ := player.MediaPosition()
pos := float64(pos32)
if duration > 0 {
timePos := pos * duration
percentPos := pos * 100
// 忽略小变化
if math.Abs(timePos-prevTimePos) < 0.5 && math.Abs(percentPos-prevPercentPos) < 0.5 {
return
}
prevTimePos = timePos
prevPercentPos = percentPos
global.EventManager.CallA(events.PlayerPropertyTimePosUpdate, events.PlayerPropertyTimePosUpdateEvent{
TimePos: timePos,
})
global.EventManager.CallA(events.PlayerPropertyPercentPosUpdate, events.PlayerPropertyPercentPosUpdateEvent{
PercentPos: percentPos,
})
}
}, nil)
if err != nil {
log.Error("register MediaPlayerPositionChanged event failed: ", err)
}
// 时间改变事件(获取时长)
_, err = eventManager.Attach(vlc.MediaPlayerTimeChanged, func(e vlc.Event, userData interface{}) {
dur, _ := player.MediaLength()
duration = float64(dur) / 1000.0 // 转换为秒
global.EventManager.CallA(events.PlayerPropertyDurationUpdate, events.PlayerPropertyDurationUpdateEvent{
Duration: duration,
})
}, nil)
if err != nil {
log.Error("register MediaPlayerTimeChanged event failed: ", err)
}
// 暂停状态改变
_, err = eventManager.Attach(vlc.MediaPlayerPaused, func(e vlc.Event, userData interface{}) {
log.Debug("VLC player paused")
global.EventManager.CallA(events.PlayerPropertyPauseUpdate, events.PlayerPropertyPauseUpdateEvent{
Paused: true,
})
}, nil)
if err != nil {
log.Error("register MediaPlayerPaused event failed: ", err)
}
_, err = eventManager.Attach(vlc.MediaPlayerPlaying, func(e vlc.Event, userData interface{}) {
log.Debug("VLC player playing")
global.EventManager.CallA(events.PlayerPropertyPauseUpdate, events.PlayerPropertyPauseUpdateEvent{
Paused: false,
})
}, nil)
if err != nil {
log.Error("register MediaPlayerPlaying event failed: ", err)
}
_, err = eventManager.Attach(vlc.MediaPlayerAudioVolume, func(e vlc.Event, userData interface{}) {
volume, _ := player.Volume()
log.Debug("VLC player audio volume: ", volume)
global.EventManager.CallA(events.PlayerPropertyVolumeUpdate, events.PlayerPropertyVolumeUpdateEvent{
Volume: float64(volume),
})
}, nil)
}
func registerCmdHandler() {
global.EventManager.RegisterA(events.PlayerPlayCmd, "player.play", func(evnt *event.Event) {
lock.Lock()
defer lock.Unlock()
mediaInfo := evnt.Data.(events.PlayerPlayCmdEvent).Media.Info
log.Infof("[VLC Player] Play media %s", mediaInfo.Title)
mediaUrls, err := miaosic.GetMediaUrl(mediaInfo.Meta, miaosic.QualityAny)
if err != nil || len(mediaUrls) == 0 {
log.Warn("[VLC PlayControl] get media url failed ", mediaInfo.Meta.ID(), err)
global.EventManager.CallA(
events.PlayerPlayErrorUpdate,
events.PlayerPlayErrorUpdateEvent{
Error: err,
})
return
}
// 创建媒体对象
var media *vlc.Media
if strings.HasPrefix(mediaUrls[0].Url, "http") {
media, err = vlc.NewMediaFromURL(mediaUrls[0].Url)
} else {
media, err = vlc.NewMediaFromPath(mediaUrls[0].Url)
}
if err != nil {
log.Error("create media failed: ", err)
return
}
// 设置HTTP头
if val, ok := mediaUrls[0].Header["User-Agent"]; ok {
err = media.AddOptions(":http-user-agent=" + val)
if err != nil {
log.Warn("add http-user-agent options failed: ", err)
}
}
if val, ok := mediaUrls[0].Header["Referer"]; ok {
err = media.AddOptions(":http-referrer=" + val)
if err != nil {
log.Warn("add http-referrer options failed: ", err)
}
}
// 更新媒体信息
mediaData := evnt.Data.(events.PlayerPlayCmdEvent).Media
if m, err := miaosic.GetMediaInfo(mediaData.Info.Meta); err == nil {
mediaData.Info = m
}
currentMedia = mediaData
global.EventManager.CallA(events.PlayerPlayingUpdate, events.PlayerPlayingUpdateEvent{
Media: mediaData,
Removed: false,
})
// 播放
if err := player.SetMedia(media); err != nil {
log.Error("set media failed: ", err)
return
}
if currentWindowHandle != 0 {
if err := setWindowHandle(currentWindowHandle); err != nil {
log.Error("apply window handle failed: ", err)
}
}
if err := player.Play(); err != nil {
log.Error("play failed: ", err)
return
}
// 重置位置信息
prevPercentPos = 0
prevTimePos = 0
global.EventManager.CallA(events.PlayerPropertyTimePosUpdate, events.PlayerPropertyTimePosUpdateEvent{
TimePos: 0,
})
global.EventManager.CallA(events.PlayerPropertyPercentPosUpdate, events.PlayerPropertyPercentPosUpdateEvent{
PercentPos: 0,
})
global.EventManager.CallA(events.PlayerPropertyIdleActiveUpdate, events.PlayerPropertyIdleActiveUpdateEvent{
IsIdle: false,
})
})
global.EventManager.RegisterA(events.PlayerToggleCmd, "player.toggle", func(evnt *event.Event) {
lock.Lock()
defer lock.Unlock()
err := player.TogglePause()
if err != nil {
log.Errorf("[VLC Player] Toggle pause failed: %v", err)
return
}
})
global.EventManager.RegisterA(events.PlayerSetPauseCmd, "player.set_paused", func(evnt *event.Event) {
lock.Lock()
defer lock.Unlock()
data := evnt.Data.(events.PlayerSetPauseCmdEvent)
err := player.SetPause(data.Pause)
if err != nil {
log.Errorf("[VLC Player] SetPause failed: %v", err)
return
}
})
global.EventManager.RegisterA(events.PlayerSeekCmd, "player.seek", func(evnt *event.Event) {
lock.Lock()
defer lock.Unlock()
data := evnt.Data.(events.PlayerSeekCmdEvent)
if data.Absolute {
player.SetMediaTime(int(data.Position * 1000)) // 转换为毫秒
} else {
player.SetMediaPosition(float32(data.Position / 100))
}
})
global.EventManager.RegisterA(events.PlayerVolumeChangeCmd, "player.volume", func(evnt *event.Event) {
lock.Lock()
defer lock.Unlock()
data := evnt.Data.(events.PlayerVolumeChangeCmdEvent)
err := player.SetVolume(int(data.Volume))
if err != nil {
log.Errorf("[VLC Player] SetVolume failed: %v", err)
}
})
global.EventManager.RegisterA(events.PlayerVideoPlayerSetWindowHandleCmd, "player.set_window_handle", func(evnt *event.Event) {
handle := evnt.Data.(events.PlayerVideoPlayerSetWindowHandleCmdEvent).Handle
setWindowHandle(handle)
})
global.EventManager.RegisterA(events.PlayerSetAudioDeviceCmd, "player.set_audio_device", func(evnt *event.Event) {
device := evnt.Data.(events.PlayerSetAudioDeviceCmdEvent).Device
if err := setAudioDevice(device); err != nil {
log.Warn("set audio device failed", err)
global.EventManager.CallA(
events.ErrorUpdate,
events.ErrorUpdateEvent{
Error: err,
})
}
})
}
// setAudioDevice 设置音频输出设备
func setAudioDevice(deviceID string) error {
lock.Lock()
defer lock.Unlock()
log.Infof("set audio device to: %s", deviceID)
// 验证设备是否在列表中
found := false
for _, dev := range audioDevices {
if dev.Name == deviceID {
found = true
break
}
}
if !found {
return fmt.Errorf("audio device not found: %s", deviceID)
}
// 设置音频设备
if err := player.SetAudioOutputDevice(deviceID, ""); err != nil {
log.Error("set audio device failed: ", err)
return err
}
currentAudioDevice = deviceID
// 更新配置
cfg.AudioDevice = deviceID
// 发送更新事件
global.EventManager.CallA(events.PlayerAudioDeviceUpdate, events.PlayerAudioDeviceUpdateEvent{
Current: currentAudioDevice,
Devices: audioDevices,
})
return nil
}
// updateAudioDeviceList 获取并更新音频设备列表
func updateAudioDeviceList() {
lock.Lock()
defer lock.Unlock()
// 获取所有音频设备
devices, err := player.AudioOutputDevices()
if err != nil {
log.Error("get audio device list failed: ", err)
return
}
// 获取当前音频设备
currentDevice, err := player.AudioOutputDevice()
if err != nil {
log.Warn("get current audio device failed: ", err)
currentDevice = ""
}
log.Debugf("current audio device list: %s", devices)
log.Debugf("current audio device: %s", currentDevice)
// 转换设备格式
audioDevices = make([]model.AudioDevice, 0, len(devices))
for _, device := range devices {
audioDevices = append(audioDevices, model.AudioDevice{
Name: device.Name,
Description: device.Description,
})
}
currentAudioDevice = currentDevice
log.Infof("update audio device list: %d devices, current: %s",
len(audioDevices), currentAudioDevice)
// 发送事件通知
global.EventManager.CallA(events.PlayerAudioDeviceUpdate, events.PlayerAudioDeviceUpdateEvent{
Current: currentAudioDevice,
Devices: audioDevices,
})
}

View File

@@ -10,7 +10,7 @@ import (
const (
ProgramName = "卡西米尔唱片机"
Version uint32 = 0x010103
Version uint32 = 0x010200
)
const (

View File

@@ -3,6 +3,7 @@ package config
type _ExperimentalConfig struct {
BaseConfig
Headless bool
PlayerCore string
}
func (c *_ExperimentalConfig) Name() string {
@@ -11,4 +12,5 @@ func (c *_ExperimentalConfig) Name() string {
var Experimental = &_ExperimentalConfig{
Headless: false,
PlayerCore: "mpv",
}

View File

@@ -12,6 +12,7 @@ type _GeneralConfig struct {
UseSystemPlaylist bool
FixedSize bool
EnableSMC bool // enable system media control
CustomFonts string // use custom fonts, under ./assets file
}
@@ -30,4 +31,5 @@ var General = &_GeneralConfig{
UseSystemPlaylist: true,
FixedSize: true,
EnableSMC: true,
CustomFonts: "",
}

View File

@@ -135,6 +135,7 @@ func (d *Diange) Enable() error {
"plugin.diange.queue.update",
func(event *event.Event) {
d.currentQueueLength = len(event.Data.(events.PlaylistDetailUpdateEvent).Medias)
d.log.Debugf("current queue length: %d", d.currentQueueLength)
medias := event.Data.(events.PlaylistDetailUpdateEvent).Medias
tmpUserCount := make(map[string]int)
for _, media := range medias {
@@ -144,8 +145,11 @@ func (d *Diange) Enable() error {
}
tmpUserCount[media.ToUser().Name]++
}
// clear user count
d.userCount.Clear()
for user, count := range tmpUserCount {
d.userCount.Store(user, count)
d.log.Debugf("user media count in player playlist %s: %d", user, count)
}
})
global.EventManager.RegisterA(

View File

@@ -1,21 +1,21 @@
- long text wrap in list
- went idle and insert new item race condition
- @3 fix handler execution (maybe priority)
- @4 list refresh
- @5 delete optimization
- race condition in RichText. i dont care fu
- 适配歌词服务器
- 媒体源 - 歌单信息获取
- mpris, SMTC
- 歌词event发送全部歌词前端处理不同版本
- mpris
- optimize local music
- 从搜索里添加的歌不能被切
- 随机歌单
- 下一首为非版权歌时自动切歌不生效
- 网易云电台节目
- 自定义弹幕服务器
----
Finished
- 2024.06.30 : 添加vlc核心修复若干buggui框架更新修复点歌限制为1时可能出现的无法点歌的问题
- 2024.05.27 : 修复web弹幕获取到0个host的时候闪退的问题
- 2024.03.27 : 添加b站合集新链接的格式
- 2024.03.22 : 添加酷狗歌单新链接的格式