GUI fields now trim leading and trailing whitespace:

This commit is contained in:
Dmitry Sergeev
2026-09-02 00:23:03 +03:00
parent e7b032ce77
commit da5ff8ea4a
9 changed files with 200 additions and 15 deletions
+5
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"mxl-player/internal/playback" "mxl-player/internal/playback"
"strings"
"time" "time"
) )
@@ -17,6 +18,10 @@ func resolveDomain(shared, override string) string {
return shared return shared
} }
func normalizeFeedInput(domain, uuid string) (string, string) {
return strings.TrimSpace(domain), strings.TrimSpace(uuid)
}
func resolveRetryPolicy( func resolveRetryPolicy(
cli playback.RetryPolicy, cli playback.RetryPolicy,
cliMaxAttemptsSet bool, cliMaxAttemptsSet bool,
+13
View File
@@ -183,3 +183,16 @@ func TestResolveRetryPolicy(t *testing.T) {
}) })
} }
} }
func TestNormalizeFeedInput(t *testing.T) {
domain, uuid := normalizeFeedInput(
" \t/dev/shm/mxl\n",
"\r 5fbec3b1-1b0f-417d-9059-8b94a47197ef \t",
)
if domain != "/dev/shm/mxl" {
t.Fatalf("domain = %q", domain)
}
if uuid != "5fbec3b1-1b0f-417d-9059-8b94a47197ef" {
t.Fatalf("UUID = %q", uuid)
}
}
+28 -1
View File
@@ -22,7 +22,7 @@ import (
const ( const (
APP_NAME = "MXL Player" APP_NAME = "MXL Player"
APP_VER = "0.1.0" APP_VER = "1.0.0"
WIN_WIDTH int32 = 1280 WIN_WIDTH int32 = 1280
WIN_HEIGHT int32 = 720 WIN_HEIGHT int32 = 720
) )
@@ -377,6 +377,8 @@ func main() {
} }
} }
doReconnect := func() { doReconnect := func() {
videoDomainStr, videoStr = normalizeFeedInput(videoDomainStr, videoStr)
audioDomainStr, audioStr = normalizeFeedInput(audioDomainStr, audioStr)
videoActive = videoStr != "" videoActive = videoStr != ""
audioActive = audioStr != "" audioActive = audioStr != ""
@@ -470,6 +472,8 @@ func main() {
displayedVideoWidth uint32 = placeholderWidth displayedVideoWidth uint32 = placeholderWidth
displayedVideoHeight uint32 = placeholderHeight displayedVideoHeight uint32 = placeholderHeight
displayedVideoStride uint32 = placeholderStride displayedVideoStride uint32 = placeholderStride
displayedVideoSource playback.FeedConfig
hasDisplayedVideo bool
fps float64 fps float64
dropTracker videoDropTracker dropTracker videoDropTracker
@@ -575,6 +579,16 @@ func main() {
} }
snapshot, hasSnapshot := player.Controller.Snapshot() snapshot, hasSnapshot := player.Controller.Snapshot()
desiredVideo := desiredVideoFeed(snapshot, hasSnapshot)
if !desiredVideo.Active ||
desiredVideo.Domain != displayedVideoSource.Domain ||
desiredVideo.UUID != displayedVideoSource.UUID {
hasDisplayedVideo = false
}
if hasFrame {
displayedVideoSource = shownSource
hasDisplayedVideo = shouldShowVideo(desiredVideo, shownSource)
}
// stats // stats
if hasFrame { if hasFrame {
@@ -722,7 +736,13 @@ func main() {
drawFeedsSections := func() { drawFeedsSections := func() {
cimgui.SeparatorText("Video") cimgui.SeparatorText("Video")
cimgui.InputTextWithHint("Video domain", "/dev/shm/mxl", &videoDomainStr, 0, nil) cimgui.InputTextWithHint("Video domain", "/dev/shm/mxl", &videoDomainStr, 0, nil)
if cimgui.IsItemDeactivatedAfterEdit() {
videoDomainStr, _ = normalizeFeedInput(videoDomainStr, "")
}
cimgui.InputTextWithHint("Video UUID", "", &videoStr, 0, nil) cimgui.InputTextWithHint("Video UUID", "", &videoStr, 0, nil)
if cimgui.IsItemDeactivatedAfterEdit() {
_, videoStr = normalizeFeedInput("", videoStr)
}
if videoActive { if videoActive {
cimgui.SameLine() cimgui.SameLine()
if cimgui.Button("Stop##video") { if cimgui.Button("Stop##video") {
@@ -747,7 +767,13 @@ func main() {
} }
cimgui.SeparatorText("Audio") cimgui.SeparatorText("Audio")
cimgui.InputTextWithHint("Audio domain", "/dev/shm/mxl", &audioDomainStr, 0, nil) cimgui.InputTextWithHint("Audio domain", "/dev/shm/mxl", &audioDomainStr, 0, nil)
if cimgui.IsItemDeactivatedAfterEdit() {
audioDomainStr, _ = normalizeFeedInput(audioDomainStr, "")
}
cimgui.InputTextWithHint("Audio UUID", "", &audioStr, 0, nil) cimgui.InputTextWithHint("Audio UUID", "", &audioStr, 0, nil)
if cimgui.IsItemDeactivatedAfterEdit() {
_, audioStr = normalizeFeedInput("", audioStr)
}
if audioActive { if audioActive {
cimgui.SameLine() cimgui.SameLine()
if cimgui.Button("Stop##audio") { if cimgui.Button("Stop##audio") {
@@ -1062,6 +1088,7 @@ func main() {
displayedVideoWidth, displayedVideoWidth,
displayedVideoHeight, displayedVideoHeight,
displayedVideoStride, displayedVideoStride,
hasDisplayedVideo,
) )
if errors.Is(err, renderer.ErrOutOfDate) { if errors.Is(err, renderer.ErrOutOfDate) {
if rerr := r.RecreateSwapchain(); rerr != nil { if rerr := r.RecreateSwapchain(); rerr != nil {
+30
View File
@@ -0,0 +1,30 @@
package main
import "mxl-player/internal/playback"
func desiredVideoFeed(
snapshot playback.SessionSnapshot,
available bool,
) playback.FeedConfig {
if !available {
return playback.FeedConfig{}
}
switch snapshot.Plan.Topology {
case playback.TopologyIndependent:
if snapshot.Plan.Video.Active {
return snapshot.Plan.Video
}
case playback.TopologySynchronized:
if snapshot.Plan.Sync.Active() {
return snapshot.Plan.Sync.Video
}
}
return playback.FeedConfig{}
}
func shouldShowVideo(
desired playback.FeedConfig,
delivered playback.FeedConfig,
) bool {
return desired.Active && sameVideoSource(desired, delivered)
}
+46
View File
@@ -0,0 +1,46 @@
package main
import (
"testing"
"mxl-player/internal/playback"
)
func TestDesiredVideoFeed(t *testing.T) {
video := playback.FeedConfig{Domain: "/video", UUID: "video", Active: true}
tests := []struct {
name string
available bool
plan playback.SessionPlan
want playback.FeedConfig
}{
{name: "snapshot unavailable"},
{name: "idle", available: true, plan: playback.SessionPlan{Topology: playback.TopologyIdle}},
{name: "audio only", available: true, plan: playback.SessionPlan{Topology: playback.TopologyIndependent}},
{name: "independent video", available: true, plan: playback.SessionPlan{Topology: playback.TopologyIndependent, Video: video}, want: video},
{name: "synchronized video", available: true, plan: playback.SessionPlan{Topology: playback.TopologySynchronized, Sync: playback.SyncPairConfig{Video: video, Audio: playback.FeedConfig{Active: true}}}, want: video},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := desiredVideoFeed(playback.SessionSnapshot{Plan: test.plan}, test.available)
if got != test.want {
t.Fatalf("desiredVideoFeed() = %#v, want %#v", got, test.want)
}
})
}
}
func TestShouldShowVideoRequiresDesiredSource(t *testing.T) {
desired := playback.FeedConfig{Domain: "/video", UUID: "video", Active: true}
if !shouldShowVideo(desired, desired) {
t.Fatal("matching active video was hidden")
}
if shouldShowVideo(playback.FeedConfig{}, desired) {
t.Fatal("video was shown without an active desired feed")
}
other := desired
other.UUID = "other"
if shouldShowVideo(desired, other) {
t.Fatal("frame from old source was shown")
}
}
+13
View File
@@ -3,6 +3,8 @@ package imgui
import ( import (
"time" "time"
"mxl-player/internal/sdl"
cimgui "github.com/AllenDang/cimgui-go/imgui" cimgui "github.com/AllenDang/cimgui-go/imgui"
) )
@@ -17,9 +19,20 @@ func New() *Context {
ctx := cimgui.CreateContext() ctx := cimgui.CreateContext()
cimgui.SetCurrentContext(ctx) cimgui.SetCurrentContext(ctx)
io := cimgui.CurrentIO() io := cimgui.CurrentIO()
cimgui.CurrentPlatformIO().SetClipboardHandler(sdlClipboardHandler{})
return &Context{ctx: ctx, io: io} return &Context{ctx: ctx, io: io}
} }
type sdlClipboardHandler struct{}
func (sdlClipboardHandler) GetClipboard() string {
return sdl.GetClipboardText()
}
func (sdlClipboardHandler) SetClipboard(text string) {
sdl.SetClipboardText(text)
}
func (c *Context) Destroy() { func (c *Context) Destroy() {
cimgui.DestroyContext() cimgui.DestroyContext()
} }
+20
View File
@@ -7,6 +7,13 @@ import (
cimgui "github.com/AllenDang/cimgui-go/imgui" cimgui "github.com/AllenDang/cimgui-go/imgui"
) )
const (
sdlKModShift uint16 = 0x0001 | 0x0002
sdlKModCtrl uint16 = 0x0040 | 0x0080
sdlKModAlt uint16 = 0x0100 | 0x0200
sdlKModGUI uint16 = 0x0400 | 0x0800
)
// SDL3 event (128 byte raw buffer) -> imgui // SDL3 event (128 byte raw buffer) -> imgui
func (c *Context) ProcessEvent(event *[128]byte) { func (c *Context) ProcessEvent(event *[128]byte) {
eventType := *(*uint32)(unsafe.Pointer(&event[0])) eventType := *(*uint32)(unsafe.Pointer(&event[0]))
@@ -15,7 +22,9 @@ func (c *Context) ProcessEvent(event *[128]byte) {
switch eventType { switch eventType {
case sdl.EventKeyDown, sdl.EventKeyUp: case sdl.EventKeyDown, sdl.EventKeyUp:
scancode := *(*uint32)(unsafe.Pointer(&event[24])) scancode := *(*uint32)(unsafe.Pointer(&event[24]))
modifiers := *(*uint16)(unsafe.Pointer(&event[32]))
down := eventType == sdl.EventKeyDown down := eventType == sdl.EventKeyDown
c.addKeyModifiers(modifiers)
key := sdlScancodeToImGuiKey(scancode) key := sdlScancodeToImGuiKey(scancode)
if key >= 0 { if key >= 0 {
c.io.AddKeyEvent(key, down) c.io.AddKeyEvent(key, down)
@@ -47,6 +56,13 @@ func (c *Context) ProcessEvent(event *[128]byte) {
} }
} }
func (c *Context) addKeyModifiers(modifiers uint16) {
c.io.AddKeyEvent(cimgui.ModCtrl, modifiers&sdlKModCtrl != 0)
c.io.AddKeyEvent(cimgui.ModShift, modifiers&sdlKModShift != 0)
c.io.AddKeyEvent(cimgui.ModAlt, modifiers&sdlKModAlt != 0)
c.io.AddKeyEvent(cimgui.ModSuper, modifiers&sdlKModGUI != 0)
}
func sdlScancodeToImGuiKey(scancode uint32) cimgui.Key { func sdlScancodeToImGuiKey(scancode uint32) cimgui.Key {
switch scancode { switch scancode {
case 40: // SDL_SCANCODE_RETURN case 40: // SDL_SCANCODE_RETURN
@@ -79,6 +95,10 @@ func sdlScancodeToImGuiKey(scancode uint32) cimgui.Key {
return cimgui.KeyLeftAlt return cimgui.KeyLeftAlt
case 230: // SDL_SCANCODE_RALT case 230: // SDL_SCANCODE_RALT
return cimgui.KeyRightAlt return cimgui.KeyRightAlt
case 227: // SDL_SCANCODE_LGUI
return cimgui.KeyLeftSuper
case 231: // SDL_SCANCODE_RGUI
return cimgui.KeyRightSuper
default: default:
// Letters A-Z: scancodes 4-29 map to ImGui KeyA-KeyZ // Letters A-Z: scancodes 4-29 map to ImGui KeyA-KeyZ
if scancode >= 4 && scancode <= 29 { if scancode >= 4 && scancode <= 29 {
+19 -12
View File
@@ -455,7 +455,12 @@ func validateFramePayload(
// DrawFrame acquires an image, records commands, submits, and presents. // DrawFrame acquires an image, records commands, submits, and presents.
// Returns ErrOutOfDate if the swapchain needs recreation // Returns ErrOutOfDate if the swapchain needs recreation
func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error { func (r *Renderer) DrawFrame(
videoW uint32,
videoH uint32,
stride uint32,
showVideo bool,
) error {
imageIndex, res := r.dev.AcquireNextImage(r.swapchain, r.imageAvailable, ^uint64(0)) imageIndex, res := r.dev.AcquireNextImage(r.swapchain, r.imageAvailable, ^uint64(0))
if res == vk.ErrorOutOfDateKHR || res == vk.SuboptimalKHR { if res == vk.ErrorOutOfDateKHR || res == vk.SuboptimalKHR {
return ErrOutOfDate return ErrOutOfDate
@@ -483,7 +488,7 @@ func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error {
r.fbs[imageIndex], r.fbs[imageIndex],
vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: r.extent}, vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: r.extent},
[]vk.ClearValue{ []vk.ClearValue{
vk.ClearColor(0.0, 0.0, 0.0, 1.0), vk.ClearColor(0.025, 0.03, 0.04, 1.0),
vk.ClearDepthStencil(1.0, 0), vk.ClearDepthStencil(1.0, 0),
}, },
) )
@@ -494,17 +499,19 @@ func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error {
MinDepth: 0, MaxDepth: 1, MinDepth: 0, MaxDepth: 1,
}) })
cmd.SetScissor(vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: r.extent}) cmd.SetScissor(vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: r.extent})
cmd.BindPipeline(r.decodePipeline) if showVideo {
cmd.BindDescriptorSet(r.decodeLayout, 0, r.decodeSet) cmd.BindPipeline(r.decodePipeline)
pc := PushConstants{ cmd.BindDescriptorSet(r.decodeLayout, 0, r.decodeSet)
Width: videoW, pc := PushConstants{
Height: videoH, Width: videoW,
StrideBytes: stride, Height: videoH,
WinW: r.extent.Width, StrideBytes: stride,
WinH: r.extent.Height, WinW: r.extent.Width,
WinH: r.extent.Height,
}
cmd.PushConstants(r.decodeLayout, vk.ShaderStageFragment, 0, unsafe.Pointer(&pc), 20)
cmd.Draw(3, 1, 0, 0)
} }
cmd.PushConstants(r.decodeLayout, vk.ShaderStageFragment, 0, unsafe.Pointer(&pc), 20)
cmd.Draw(3, 1, 0, 0)
if r.ImGuiDraw != nil { if r.ImGuiDraw != nil {
r.ImGuiDraw(cmd) r.ImGuiDraw(cmd)
} }
+26 -2
View File
@@ -63,8 +63,11 @@ var (
sdlGetAudioPlaybackDevices func(count *int32) uintptr sdlGetAudioPlaybackDevices func(count *int32) uintptr
sdlGetAudioDeviceName func(devid uint32) uintptr sdlGetAudioDeviceName func(devid uint32) uintptr
sdlStartTextInput func(window uintptr) sdlStartTextInput func(window uintptr)
sdlStopTextInput func(window uintptr) sdlStopTextInput func(window uintptr)
sdlGetClipboardText func() uintptr
sdlSetClipboardText func(text *byte) bool
sdlFree func(memory uintptr)
) )
var loaded = false var loaded = false
@@ -98,6 +101,9 @@ func Load() error {
// input // input
purego.RegisterLibFunc(&sdlStartTextInput, h, "SDL_StartTextInput") purego.RegisterLibFunc(&sdlStartTextInput, h, "SDL_StartTextInput")
purego.RegisterLibFunc(&sdlStopTextInput, h, "SDL_StopTextInput") purego.RegisterLibFunc(&sdlStopTextInput, h, "SDL_StopTextInput")
purego.RegisterLibFunc(&sdlGetClipboardText, h, "SDL_GetClipboardText")
purego.RegisterLibFunc(&sdlSetClipboardText, h, "SDL_SetClipboardText")
purego.RegisterLibFunc(&sdlFree, h, "SDL_free")
loaded = true loaded = true
return nil return nil
} }
@@ -197,3 +203,21 @@ func GetAudioPlaybackDevices() []AudioDevice {
// Input wrappers // Input wrappers
func StartTextInput(window uintptr) { sdlStartTextInput(window) } func StartTextInput(window uintptr) { sdlStartTextInput(window) }
func StopTextInput(window uintptr) { sdlStopTextInput(window) } func StopTextInput(window uintptr) { sdlStopTextInput(window) }
func GetClipboardText() string {
text := sdlGetClipboardText()
if text == 0 {
return ""
}
result := cstr(text)
sdlFree(text)
return result
}
func SetClipboardText(text string) bool {
bytes := make([]byte, len(text)+1)
copy(bytes, text)
result := sdlSetClipboardText(&bytes[0])
runtime.KeepAlive(bytes)
return result
}