Merge pull request #533 from dweymouth/feature/windows-smtc
Windows SMTC (System Media Transport Controls) integration
This commit is contained in:
@@ -32,7 +32,7 @@ jobs:
|
|||||||
./appimage-build-compat.sh
|
./appimage-build-compat.sh
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Supersonic-compat.AppImage
|
name: Supersonic-compat.AppImage
|
||||||
path: Supersonic-x86_64.AppImage
|
path: Supersonic-x86_64.AppImage
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
./appimage-build.sh
|
./appimage-build.sh
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Supersonic.AppImage
|
name: Supersonic.AppImage
|
||||||
path: Supersonic-x86_64.AppImage
|
path: Supersonic-x86_64.AppImage
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
make zip_macos
|
make zip_macos
|
||||||
|
|
||||||
- name: Upload package
|
- name: Upload package
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Supersonic.zip
|
name: Supersonic.zip
|
||||||
path: Supersonic.zip
|
path: Supersonic.zip
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ jobs:
|
|||||||
mv Supersonic.zip Supersonic_HighSierra+.zip
|
mv Supersonic.zip Supersonic_HighSierra+.zip
|
||||||
|
|
||||||
- name: Upload Big Sur+ package
|
- name: Upload Big Sur+ package
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Supersonic_mac_x64_BigSur+.zip
|
name: Supersonic_mac_x64_BigSur+.zip
|
||||||
path: Supersonic_BigSur+.zip
|
path: Supersonic_BigSur+.zip
|
||||||
|
|
||||||
- name: Upload High Sierra+ package
|
- name: Upload High Sierra+ package
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Supersonic_mac_x64_HighSierra+.zip
|
name: Supersonic_mac_x64_HighSierra+.zip
|
||||||
path: Supersonic_HighSierra+.zip
|
path: Supersonic_HighSierra+.zip
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
run: make package_linux
|
run: make package_linux
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Supersonic_ubuntu_x64.tar.xz
|
name: Supersonic_ubuntu_x64.tar.xz
|
||||||
path: Supersonic.tar.xz
|
path: Supersonic.tar.xz
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
run: make package_linux
|
run: make package_linux
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Supersonic_ubuntu_x64.tar.xz
|
name: Supersonic_ubuntu_x64.tar.xz
|
||||||
path: Supersonic.tar.xz
|
path: Supersonic.tar.xz
|
||||||
|
|||||||
@@ -53,8 +53,12 @@ jobs:
|
|||||||
rm Supersonic.exe &&
|
rm Supersonic.exe &&
|
||||||
mv Supersonic-newbuild.exe Supersonic.exe
|
mv Supersonic-newbuild.exe Supersonic.exe
|
||||||
|
|
||||||
|
- name: Download smtc dll
|
||||||
|
run: >
|
||||||
|
wget https://github.com/supersonic-app/smtc-dll/releases/download/v0.1.0/SMTC.dll
|
||||||
|
|
||||||
- name: Generate zip bundle
|
- name: Generate zip bundle
|
||||||
run: zip Supersonic-windows.zip Supersonic.exe libmpv-2.dll
|
run: zip Supersonic-windows.zip Supersonic.exe libmpv-2.dll SMTC.dll
|
||||||
|
|
||||||
- name: Generate installer
|
- name: Generate installer
|
||||||
uses: Minionguyjpro/Inno-Setup-Action@v1.2.2
|
uses: Minionguyjpro/Inno-Setup-Action@v1.2.2
|
||||||
@@ -62,13 +66,13 @@ jobs:
|
|||||||
path: win_inno_installscript.iss
|
path: win_inno_installscript.iss
|
||||||
|
|
||||||
- name: Upload zip
|
- name: Upload zip
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Supersonic_windows_x64.zip
|
name: Supersonic_windows_x64.zip
|
||||||
path: Supersonic-windows.zip
|
path: Supersonic-windows.zip
|
||||||
|
|
||||||
- name: Upload installer
|
- name: Upload installer
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Supersonic_windows_x64_installer.exe
|
name: Supersonic_windows_x64_installer.exe
|
||||||
path: Output/supersonic-installer.exe
|
path: Output/supersonic-installer.exe
|
||||||
|
|||||||
+81
-11
@@ -12,6 +12,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dweymouth/supersonic/backend/ipc"
|
"github.com/dweymouth/supersonic/backend/ipc"
|
||||||
@@ -45,17 +46,19 @@ type App struct {
|
|||||||
LocalPlayer *mpv.Player
|
LocalPlayer *mpv.Player
|
||||||
UpdateChecker UpdateChecker
|
UpdateChecker UpdateChecker
|
||||||
MPRISHandler *MPRISHandler
|
MPRISHandler *MPRISHandler
|
||||||
|
WinSMTC *SMTC
|
||||||
ipcServer ipc.IPCServer
|
ipcServer ipc.IPCServer
|
||||||
|
|
||||||
// UI callbacks to be set in main
|
// UI callbacks to be set in main
|
||||||
OnReactivate func()
|
OnReactivate func()
|
||||||
OnExit func()
|
OnExit func()
|
||||||
|
|
||||||
appName string
|
appName string
|
||||||
appVersionTag string
|
displayAppName string
|
||||||
configDir string
|
appVersionTag string
|
||||||
cacheDir string
|
configDir string
|
||||||
portableMode bool
|
cacheDir string
|
||||||
|
portableMode bool
|
||||||
|
|
||||||
isFirstLaunch bool // set by config file reader
|
isFirstLaunch bool // set by config file reader
|
||||||
bgrndCtx context.Context
|
bgrndCtx context.Context
|
||||||
@@ -95,12 +98,13 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
|
|||||||
}
|
}
|
||||||
|
|
||||||
a := &App{
|
a := &App{
|
||||||
logFile: logFile,
|
logFile: logFile,
|
||||||
appName: appName,
|
appName: appName,
|
||||||
appVersionTag: appVersionTag,
|
displayAppName: displayAppName,
|
||||||
configDir: confDir,
|
appVersionTag: appVersionTag,
|
||||||
cacheDir: cacheDir,
|
configDir: confDir,
|
||||||
portableMode: portableMode,
|
cacheDir: cacheDir,
|
||||||
|
portableMode: portableMode,
|
||||||
}
|
}
|
||||||
a.bgrndCtx, a.cancel = context.WithCancel(context.Background())
|
a.bgrndCtx, a.cancel = context.WithCancel(context.Background())
|
||||||
a.readConfig()
|
a.readConfig()
|
||||||
@@ -165,11 +169,14 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OS media center integrations
|
// OS media center integrations
|
||||||
|
// Linux MPRIS
|
||||||
a.setupMPRIS(displayAppName)
|
a.setupMPRIS(displayAppName)
|
||||||
|
// MacOS MPNowPlayingInfoCenter
|
||||||
InitMPMediaHandler(a.PlaybackManager, func(id string) (string, error) {
|
InitMPMediaHandler(a.PlaybackManager, func(id string) (string, error) {
|
||||||
a.ImageManager.GetCoverThumbnail(id) // ensure image is cached locally
|
a.ImageManager.GetCoverThumbnail(id) // ensure image is cached locally
|
||||||
return a.ImageManager.GetCoverArtUrl(id)
|
return a.ImageManager.GetCoverArtUrl(id)
|
||||||
})
|
})
|
||||||
|
// Windows SMTC is initialized from main once we have a window HWND.
|
||||||
|
|
||||||
a.startConfigWriter(a.bgrndCtx)
|
a.startConfigWriter(a.bgrndCtx)
|
||||||
|
|
||||||
@@ -331,6 +338,66 @@ func (a *App) setupMPRIS(mprisAppName string) {
|
|||||||
a.MPRISHandler.Start()
|
a.MPRISHandler.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) SetupWindowsSMTC(hwnd uintptr) {
|
||||||
|
smtc, err := InitSMTCForWindow(hwnd)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error initializing SMTC: %d", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.WinSMTC = smtc
|
||||||
|
smtc.UpdateMetadata(a.displayAppName, "")
|
||||||
|
|
||||||
|
smtc.OnButtonPressed(func(btn SMTCButton) {
|
||||||
|
switch btn {
|
||||||
|
case SMTCButtonPlay:
|
||||||
|
a.PlaybackManager.Continue()
|
||||||
|
case SMTCButtonPause:
|
||||||
|
a.PlaybackManager.Pause()
|
||||||
|
case SMTCButtonNext:
|
||||||
|
a.PlaybackManager.SeekNext()
|
||||||
|
case SMTCButtonPrevious:
|
||||||
|
a.PlaybackManager.SeekBackOrPrevious()
|
||||||
|
case SMTCButtonStop:
|
||||||
|
a.PlaybackManager.Stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
smtc.OnSeek(func(millis int) {
|
||||||
|
a.PlaybackManager.SeekSeconds(float64(millis) / 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
a.PlaybackManager.OnSongChange(func(nowPlaying mediaprovider.MediaItem, _ *mediaprovider.Track) {
|
||||||
|
if nowPlaying == nil {
|
||||||
|
smtc.UpdateMetadata("Supersonic", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
meta := nowPlaying.Metadata()
|
||||||
|
smtc.UpdateMetadata(meta.Name, strings.Join(meta.Artists, ", "))
|
||||||
|
smtc.UpdatePosition(0, meta.Duration*1000)
|
||||||
|
go func() {
|
||||||
|
a.ImageManager.GetCoverThumbnail(meta.CoverArtID) // ensure image is cached locally
|
||||||
|
if path, err := a.ImageManager.GetCoverArtPath(meta.CoverArtID); err == nil {
|
||||||
|
smtc.SetThumbnail(path)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
a.PlaybackManager.OnSeek(func() {
|
||||||
|
dur := a.PlaybackManager.NowPlaying().Metadata().Duration
|
||||||
|
smtc.UpdatePosition(int(a.PlaybackManager.CurrentPlayer().GetStatus().TimePos*1000), dur*1000)
|
||||||
|
})
|
||||||
|
a.PlaybackManager.OnPlaying(func() {
|
||||||
|
smtc.SetEnabled(true)
|
||||||
|
smtc.UpdatePlaybackState(SMTCPlaybackStatePlaying)
|
||||||
|
})
|
||||||
|
a.PlaybackManager.OnPaused(func() {
|
||||||
|
smtc.SetEnabled(true)
|
||||||
|
smtc.UpdatePlaybackState(SMTCPlaybackStatePaused)
|
||||||
|
})
|
||||||
|
a.PlaybackManager.OnStopped(func() {
|
||||||
|
smtc.SetEnabled(false)
|
||||||
|
smtc.UpdatePlaybackState(SMTCPlaybackStateStopped)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) LoginToDefaultServer(string) error {
|
func (a *App) LoginToDefaultServer(string) error {
|
||||||
serverCfg := a.ServerManager.GetDefaultServer()
|
serverCfg := a.ServerManager.GetDefaultServer()
|
||||||
if serverCfg == nil {
|
if serverCfg == nil {
|
||||||
@@ -370,6 +437,9 @@ func (a *App) Shutdown() {
|
|||||||
a.ipcServer.Shutdown(a.bgrndCtx)
|
a.ipcServer.Shutdown(a.bgrndCtx)
|
||||||
}
|
}
|
||||||
a.MPRISHandler.Shutdown()
|
a.MPRISHandler.Shutdown()
|
||||||
|
if a.WinSMTC != nil {
|
||||||
|
a.WinSMTC.Shutdown()
|
||||||
|
}
|
||||||
a.PlaybackManager.DisableCallbacks()
|
a.PlaybackManager.DisableCallbacks()
|
||||||
a.PlaybackManager.Stop() // will trigger scrobble check
|
a.PlaybackManager.Stop() // will trigger scrobble check
|
||||||
a.cancel()
|
a.cancel()
|
||||||
|
|||||||
+18
-2
@@ -12,6 +12,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -160,6 +161,15 @@ func (i *ImageManager) GetCoverArtUrl(coverID string) (string, error) {
|
|||||||
return "", errors.New("cover not found")
|
return "", errors.New("cover not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCoverArtPath returns the file path for the locally cached cover thumbnail, if it exists.
|
||||||
|
func (i *ImageManager) GetCoverArtPath(coverID string) (string, error) {
|
||||||
|
path := i.filePathForCover(coverID)
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
return "", errors.New("cover not found")
|
||||||
|
}
|
||||||
|
|
||||||
// GetCachedArtistImage returns the artist image for the given artistID from the on-disc cache, if it exists.
|
// GetCachedArtistImage returns the artist image for the given artistID from the on-disc cache, if it exists.
|
||||||
func (i *ImageManager) GetCachedArtistImage(artistID string) (image.Image, bool) {
|
func (i *ImageManager) GetCachedArtistImage(artistID string) (image.Image, bool) {
|
||||||
return i.loadLocalImage(i.filePathForArtistImage(artistID))
|
return i.loadLocalImage(i.filePathForArtistImage(artistID))
|
||||||
@@ -306,11 +316,11 @@ func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, coverID string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *ImageManager) filePathForCover(coverID string) string {
|
func (i *ImageManager) filePathForCover(coverID string) string {
|
||||||
return filepath.Join(i.ensureCoverCacheDir(), fmt.Sprintf("%s.jpg", coverID))
|
return filepath.Join(i.ensureCoverCacheDir(), fmt.Sprintf("%s.jpg", sanitizeFileName(coverID)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ImageManager) filePathForArtistImage(id string) string {
|
func (i *ImageManager) filePathForArtistImage(id string) string {
|
||||||
return filepath.Join(i.ensureArtistCoverCacheDir(), fmt.Sprintf("%s.jpg", id))
|
return filepath.Join(i.ensureArtistCoverCacheDir(), fmt.Sprintf("%s.jpg", sanitizeFileName(id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ImageManager) writeJpeg(img image.Image, path string) error {
|
func (i *ImageManager) writeJpeg(img image.Image, path string) error {
|
||||||
@@ -391,3 +401,9 @@ func (im *ImageManager) pruneOnDiskCache() {
|
|||||||
}
|
}
|
||||||
im.filesWrittenSinceLastPrune = false
|
im.filesWrittenSinceLastPrune = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var illegalFilename = regexp.MustCompile("[" + regexp.QuoteMeta("`~!@#$%^&*+={}|/\\:;\"'<>?") + "]")
|
||||||
|
|
||||||
|
func sanitizeFileName(s string) string {
|
||||||
|
return illegalFilename.ReplaceAllString(s, "_")
|
||||||
|
}
|
||||||
|
|||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package backend
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -I .
|
||||||
|
void btn_callback_cgo(int in);
|
||||||
|
void seek_callback_cgo(int in);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SMTCPlaybackState int
|
||||||
|
type SMTCButton int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// constants from smtc.h in github.com/supersonic-app/smtc-dll
|
||||||
|
SMTCPlaybackStateStopped SMTCPlaybackState = 2
|
||||||
|
SMTCPlaybackStatePlaying SMTCPlaybackState = 3
|
||||||
|
SMTCPlaybackStatePaused SMTCPlaybackState = 4
|
||||||
|
|
||||||
|
SMTCButtonPlay SMTCButton = 0
|
||||||
|
SMTCButtonPause SMTCButton = 1
|
||||||
|
SMTCButtonStop SMTCButton = 2
|
||||||
|
SMTCButtonPrevious SMTCButton = 4
|
||||||
|
SMTCButtonNext SMTCButton = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
type SMTC struct {
|
||||||
|
dll *windows.DLL
|
||||||
|
|
||||||
|
onButtonPressed func(SMTCButton)
|
||||||
|
onSeek func(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
var smtcInstance *SMTC
|
||||||
|
|
||||||
|
func InitSMTCForWindow(hwnd uintptr) (*SMTC, error) {
|
||||||
|
if maj, _, _ := windows.RtlGetNtVersionNumbers(); maj < 10 {
|
||||||
|
return nil, errors.New("SMTC is not supported on Windows versions < 10")
|
||||||
|
}
|
||||||
|
|
||||||
|
dll, err := windows.LoadDLL("smtc.dll")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, err := dll.FindProc("InitializeForWindow")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hr, _, _ := proc.Call(hwnd, uintptr(unsafe.Pointer(C.btn_callback_cgo)), uintptr(unsafe.Pointer(C.seek_callback_cgo)))
|
||||||
|
if hr < 0 {
|
||||||
|
return nil, fmt.Errorf("InitializeForWindow failed with HRESULT=%d", hr)
|
||||||
|
}
|
||||||
|
|
||||||
|
smtcInstance = &SMTC{dll: dll}
|
||||||
|
return smtcInstance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) OnButtonPressed(f func(SMTCButton)) {
|
||||||
|
s.onButtonPressed = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) OnSeek(f func(millis int)) {
|
||||||
|
s.onSeek = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) Shutdown() {
|
||||||
|
if s.dll == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proc, err := s.dll.FindProc("Destroy")
|
||||||
|
if err == nil {
|
||||||
|
proc.Call()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.dll.Release()
|
||||||
|
s.dll = nil
|
||||||
|
smtcInstance = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) UpdatePlaybackState(state SMTCPlaybackState) error {
|
||||||
|
if s.dll == nil {
|
||||||
|
return errors.New("SMTC DLL not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, err := s.dll.FindProc("SetPlaybackState")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hr, _, _ := proc.Call(uintptr(state)); hr < 0 {
|
||||||
|
return fmt.Errorf("SetPlaybackState failed with HRESULT=%d", hr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) UpdateMetadata(title, artist string) error {
|
||||||
|
if s.dll == nil {
|
||||||
|
return errors.New("SMTC DLL not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
utfTitle, err := windows.UTF16PtrFromString(title)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
utfArtist, err := windows.UTF16PtrFromString(artist)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, err := s.dll.FindProc("SetMetadata")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hr, _, _ := proc.Call(uintptr(unsafe.Pointer(utfTitle)), uintptr(unsafe.Pointer(utfArtist)))
|
||||||
|
if hr < 0 {
|
||||||
|
return fmt.Errorf("SetMetadata failed with HRESULT=%d", hr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) UpdatePosition(positionMillis, durationMillis int) error {
|
||||||
|
if s.dll == nil {
|
||||||
|
return errors.New("SMTC DLL not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, err := s.dll.FindProc("SetPosition")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hr, _, _ := proc.Call(uintptr(positionMillis), uintptr(durationMillis))
|
||||||
|
if hr < 0 {
|
||||||
|
return fmt.Errorf("SetPosition failed with HRESULT=%d", hr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) SetThumbnail(filepath string) error {
|
||||||
|
if s.dll == nil {
|
||||||
|
return errors.New("SMTC DLL not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, err := s.dll.FindProc("SetThumbnailPath")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
utfPath, err := windows.UTF16PtrFromString(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hr, _, _ := proc.Call(uintptr(unsafe.Pointer(utfPath)))
|
||||||
|
if hr < 0 {
|
||||||
|
return fmt.Errorf("SetThumbnailPath failed with HRESULT=%d", hr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) SetEnabled(enabled bool) error {
|
||||||
|
if s.dll == nil {
|
||||||
|
return errors.New("SMTC DLL not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, err := s.dll.FindProc("SetEnabled")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var arg uintptr = 0
|
||||||
|
if enabled {
|
||||||
|
arg = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
hr, _, _ := proc.Call(arg)
|
||||||
|
if hr < 0 {
|
||||||
|
return fmt.Errorf("SetEnabled failed with HRESULT=%d", hr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//export btnCallback
|
||||||
|
func btnCallback(in int) {
|
||||||
|
if smtcInstance != nil && smtcInstance.onButtonPressed != nil {
|
||||||
|
smtcInstance.onButtonPressed(SMTCButton(in))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export seekCallback
|
||||||
|
func seekCallback(millis int) {
|
||||||
|
if smtcInstance != nil && smtcInstance.onSeek != nil {
|
||||||
|
smtcInstance.onSeek(millis)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package backend
|
||||||
|
|
||||||
|
/*
|
||||||
|
void btn_callback_cgo(int in) {
|
||||||
|
void btnCallback(int);
|
||||||
|
btnCallback(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
void seek_callback_cgo(int in) {
|
||||||
|
void seekCallback(int);
|
||||||
|
seekCallback(in);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
type SMTCPlaybackState int
|
||||||
|
type SMTCButton int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// constants from smtc.h in github.com/supersonic-app/smtc-dll
|
||||||
|
SMTCPlaybackStateStopped SMTCPlaybackState = 2
|
||||||
|
SMTCPlaybackStatePlaying SMTCPlaybackState = 3
|
||||||
|
SMTCPlaybackStatePaused SMTCPlaybackState = 4
|
||||||
|
|
||||||
|
SMTCButtonPlay SMTCButton = 0
|
||||||
|
SMTCButtonPause SMTCButton = 1
|
||||||
|
SMTCButtonStop SMTCButton = 2
|
||||||
|
SMTCButtonPrevious SMTCButton = 4
|
||||||
|
SMTCButtonNext SMTCButton = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
type SMTC struct{}
|
||||||
|
|
||||||
|
var smtcUnsupportedErr = errors.New("SMTC is not supported on this platformo")
|
||||||
|
|
||||||
|
func InitSMTCForWindow(hwnd uintptr) (*SMTC, error) {
|
||||||
|
return nil, smtcUnsupportedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) SetEnabled(enabled bool) error {
|
||||||
|
return smtcUnsupportedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) SetThumbnail(filepath string) error {
|
||||||
|
return smtcUnsupportedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) OnButtonPressed(func(SMTCButton)) {}
|
||||||
|
|
||||||
|
func (s *SMTC) OnSeek(f func(millis int)) {}
|
||||||
|
|
||||||
|
func (s *SMTC) Shutdown() {}
|
||||||
|
|
||||||
|
func (s *SMTC) UpdatePlaybackState(state SMTCPlaybackState) error {
|
||||||
|
return smtcUnsupportedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) UpdateMetadata(title, artist string) error {
|
||||||
|
return smtcUnsupportedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTC) UpdatePosition(positionMillis, durationMillis int) error {
|
||||||
|
return smtcUnsupportedErr
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ require (
|
|||||||
github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d
|
github.com/supersonic-app/go-subsonic v0.0.0-20241224013245-9b2841f3711d
|
||||||
github.com/zalando/go-keyring v0.2.6
|
github.com/zalando/go-keyring v0.2.6
|
||||||
golang.org/x/net v0.25.0
|
golang.org/x/net v0.25.0
|
||||||
|
golang.org/x/sys v0.26.0
|
||||||
golang.org/x/text v0.16.0
|
golang.org/x/text v0.16.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +51,6 @@ require (
|
|||||||
github.com/yuin/goldmark v1.7.1 // indirect
|
github.com/yuin/goldmark v1.7.1 // indirect
|
||||||
golang.org/x/image v0.18.0 // indirect
|
golang.org/x/image v0.18.0 // indirect
|
||||||
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
|
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -87,27 +87,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// slightly hacky workaround for https://github.com/fyne-io/fyne/issues/4964
|
startupOnceTasks := sync.OnceFunc(func() {
|
||||||
if runtime.GOOS == "linux" {
|
mainWindow.Window.(driver.NativeWindow).RunNative(func(ctx any) {
|
||||||
workaroundWindowSize := sync.OnceFunc(func() {
|
// intialize Windows SMTC
|
||||||
go func() {
|
if runtime.GOOS == "windows" {
|
||||||
isWayland := false
|
hwnd := ctx.(driver.WindowsWindowContext).HWND
|
||||||
mainWindow.Window.(driver.NativeWindow).RunNative(func(ctx any) {
|
myApp.SetupWindowsSMTC(hwnd)
|
||||||
_, isWayland = ctx.(*driver.WaylandWindowContext)
|
}
|
||||||
})
|
|
||||||
if !isWayland {
|
// slightly hacky workaround for https://github.com/fyne-io/fyne/issues/4964
|
||||||
|
_, isWayland := ctx.(*driver.WaylandWindowContext)
|
||||||
|
if runtime.GOOS == "linux" && !isWayland {
|
||||||
|
go func() {
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
s := mainWindow.DesiredSize()
|
s := mainWindow.DesiredSize()
|
||||||
mainWindow.Window.Resize(s.Subtract(fyne.NewSize(4, 0)))
|
mainWindow.Window.Resize(s.Subtract(fyne.NewSize(4, 0)))
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
mainWindow.Window.Resize(s) // back to desired size
|
mainWindow.Window.Resize(s) // back to desired size
|
||||||
}
|
}()
|
||||||
}()
|
}
|
||||||
})
|
})
|
||||||
fyneApp.Lifecycle().SetOnEnteredForeground(func() {
|
})
|
||||||
workaroundWindowSize()
|
fyneApp.Lifecycle().SetOnEnteredForeground(func() {
|
||||||
})
|
startupOnceTasks()
|
||||||
}
|
})
|
||||||
|
|
||||||
mainWindow.ShowAndRun()
|
mainWindow.ShowAndRun()
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
|
|||||||
[Files]
|
[Files]
|
||||||
Source: "{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "libmpv-2.dll"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "libmpv-2.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "SMTC.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
|
|||||||
Reference in New Issue
Block a user