GUI fields now trim leading and trailing whitespace:
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,6 +499,7 @@ 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})
|
||||||
|
if showVideo {
|
||||||
cmd.BindPipeline(r.decodePipeline)
|
cmd.BindPipeline(r.decodePipeline)
|
||||||
cmd.BindDescriptorSet(r.decodeLayout, 0, r.decodeSet)
|
cmd.BindDescriptorSet(r.decodeLayout, 0, r.decodeSet)
|
||||||
pc := PushConstants{
|
pc := PushConstants{
|
||||||
@@ -505,6 +511,7 @@ func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error {
|
|||||||
}
|
}
|
||||||
cmd.PushConstants(r.decodeLayout, vk.ShaderStageFragment, 0, unsafe.Pointer(&pc), 20)
|
cmd.PushConstants(r.decodeLayout, vk.ShaderStageFragment, 0, unsafe.Pointer(&pc), 20)
|
||||||
cmd.Draw(3, 1, 0, 0)
|
cmd.Draw(3, 1, 0, 0)
|
||||||
|
}
|
||||||
if r.ImGuiDraw != nil {
|
if r.ImGuiDraw != nil {
|
||||||
r.ImGuiDraw(cmd)
|
r.ImGuiDraw(cmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ var (
|
|||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user