GUI, but still shit a bit

This commit is contained in:
Dmitry Sergeev
2026-08-25 22:33:36 +03:00
parent b0bcd879e3
commit 9889054b9f
9 changed files with 276 additions and 128 deletions
+141 -69
View File
@@ -17,7 +17,6 @@ import (
cimgui "github.com/AllenDang/cimgui-go/imgui" cimgui "github.com/AllenDang/cimgui-go/imgui"
vk "github.com/christerso/vulkan-go/vk" vk "github.com/christerso/vulkan-go/vk"
"github.com/qvest-digital/go-mxl/mxl"
pflag "github.com/spf13/pflag" pflag "github.com/spf13/pflag"
) )
@@ -78,8 +77,10 @@ func checkMXLargs(args appArgs) {
} }
func main() { func main() {
// video: 5fbec3b1-1b0f-417d-9059-8b94a47197ed // timelapse video: 5fbec3b1-1b0f-417d-9059-8b94a47197ed
// audio: 5fbec3b1-1b0f-417d-9059-8b94a47197ec // timelapse audio: 5fbec3b1-1b0f-417d-9059-8b94a47197ec
// f1 video: 5fbec3b1-1b0f-417d-9059-8b94a47197ef
// f1 audio: 5fbec3b1-1b0f-417d-9059-8b94a47197eb
var args appArgs var args appArgs
flagSet := pflag.NewFlagSet(APP_NAME, pflag.ContinueOnError) flagSet := pflag.NewFlagSet(APP_NAME, pflag.ContinueOnError)
flagSet.Usage = func() { printUsage(os.Stderr) } flagSet.Usage = func() { printUsage(os.Stderr) }
@@ -259,7 +260,6 @@ func main() {
fmt.Printf("sync: video %dx%d audio %dch batch=%d\n", fmt.Printf("sync: video %dx%d audio %dch batch=%d\n",
syncSrc.Width(), syncSrc.Height(), aChans, audioBatch) syncSrc.Width(), syncSrc.Height(), aChans, audioBatch)
case args.VideoFlowId != "": case args.VideoFlowId != "":
videoSrc, err = source.Open(args.Domain, args.VideoFlowId) videoSrc, err = source.Open(args.Domain, args.VideoFlowId)
if err != nil { if err != nil {
log.Fatalf("source: %v", err) log.Fatalf("source: %v", err)
@@ -338,30 +338,43 @@ func main() {
defer vkDevice.WaitIdle() defer vkDevice.WaitIdle()
} }
// GUI state (accessible from doReconnect + goroutine)
var (
domainStr string = args.Domain
videoStr string = args.VideoFlowId
audioStr string = args.AudioFlowId
showStats bool = true
)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
grant := make(chan struct{}, 1) type reconnectParams struct {
domain string
video string
audio string
}
// One control channel: grant (empty params) or reconnect (with params).
control := make(chan reconnectParams, 1)
staged := make(chan uint64) staged := make(chan uint64)
failed := make(chan struct{})
reopen := func() error { reopen := func(params reconnectParams) error {
// Close current sources
if syncSrc != nil { if syncSrc != nil {
_ = syncSrc.Close() _ = syncSrc.Close()
syncSrc = nil
} }
if videoSrc != nil { if videoSrc != nil {
_ = videoSrc.Close() _ = videoSrc.Close()
videoSrc = nil
} }
if audioSrc != nil { if audioSrc != nil {
_ = audioSrc.Close() _ = audioSrc.Close()
audioSrc = nil
} }
for { // Try once. Return error if fails — caller loops back to select
select { // and can pick up new reconnect params or a new grant.
case <-ctx.Done(): if params.video != "" && params.audio != "" {
return ctx.Err() s, e := source.OpenSync(params.domain, params.video, params.audio)
default:
}
if syncSrc != nil {
s, e := source.OpenSync(args.Domain, args.VideoFlowId, args.AudioFlowId)
if e == nil { if e == nil {
if r != nil { if r != nil {
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height()) newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
@@ -372,11 +385,16 @@ func main() {
} }
} }
syncSrc = s syncSrc = s
aChans = s.Channels()
audioBatch = uint64(s.AudioRate().Num) / uint64(s.Rate().Num)
if audioBatch == 0 {
audioBatch = 1
}
return nil return nil
} }
log.Printf("source: reopen retry: %v", e) return e
} else if videoSrc != nil { } else if params.video != "" {
s, e := source.Open(args.Domain, args.VideoFlowId) s, e := source.Open(params.domain, params.video)
if e == nil { if e == nil {
if r != nil { if r != nil {
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height()) newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
@@ -389,17 +407,29 @@ func main() {
videoSrc = s videoSrc = s
return nil return nil
} }
log.Printf("source: reopen retry: %v", e) return e
} else if audioSrc != nil { } else if params.audio != "" {
s, e := source.OpenAudio(args.Domain, args.AudioFlowId) s, e := source.OpenAudio(params.domain, params.audio)
if e == nil { if e == nil {
audioSrc = s audioSrc = s
aChans = s.Channels()
audioBatch = uint64(s.Rate().Num) / (100 * uint64(s.Rate().Den))
if audioBatch == 0 {
audioBatch = 1
}
return nil return nil
} }
log.Printf("source: reopen retry: %v", e) return e
} }
time.Sleep(500 * time.Millisecond) return fmt.Errorf("reopen: no flow specified")
} }
doReconnect := func() {
select {
case <-control:
default:
}
control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}
} }
go func() { go func() {
@@ -409,6 +439,25 @@ func main() {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case params := <-control:
if params.video != "" || params.audio != "" {
if rerr := reopen(params); rerr != nil {
if errors.Is(rerr, context.Canceled) {
return
}
log.Printf("source: reopen failed: %v, retrying", rerr)
select {
case <-time.After(500 * time.Millisecond):
case <-ctx.Done():
return
}
select {
case control <- params:
default:
}
}
}
continue
default: default:
} }
queued := sdl.GetAudioStreamQueued(audioStream) queued := sdl.GetAudioStreamQueued(audioStream)
@@ -426,19 +475,23 @@ func main() {
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return return
} }
if errors.Is(err, mxl.ErrFlowInvalid) { log.Printf("source: %v", err)
log.Printf("source: flow invalid, reopening") params := reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}
if rerr := reopen(); rerr != nil { select {
log.Printf("source: reopen failed: %v", rerr) case <-control:
cancel() default:
}
select {
case <-time.After(500 * time.Millisecond):
case <-ctx.Done():
return return
} }
select {
case control <- params:
default:
}
continue continue
} }
log.Printf("source: %v", err)
cancel()
return
}
if f.Samples != nil && audioStream != 0 { if f.Samples != nil && audioStream != 0 {
sdl.PutAudioStreamData(audioStream, interleaveAudio(f.Samples)) sdl.PutAudioStreamData(audioStream, interleaveAudio(f.Samples))
} }
@@ -447,11 +500,30 @@ func main() {
// Video (with or without sync) mode: grant/staged handshake. // Video (with or without sync) mode: grant/staged handshake.
for { for {
params := <-control
if params.video != "" || params.audio != "" {
// Reconnect request from Connect button or auto-retry.
select { select {
case <-grant: case <-control: // drain any pending grant
default:
}
if rerr := reopen(params); rerr != nil {
if errors.Is(rerr, context.Canceled) {
return
}
log.Printf("source: reopen failed: %v, retrying", rerr)
select {
case <-time.After(500 * time.Millisecond):
case <-ctx.Done(): case <-ctx.Done():
return return
} }
select {
case control <- params:
default:
}
}
continue
}
var payload []byte var payload []byte
var grainIdx uint64 var grainIdx uint64
@@ -462,24 +534,19 @@ func main() {
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return return
} }
if errors.Is(err, mxl.ErrFlowInvalid) { log.Printf("source: %v", err)
log.Printf("source: flow invalid, reopening") // Drain any pending grant, then send reconnect.
if rerr := reopen(); rerr != nil { select {
log.Printf("source: reopen failed: %v", rerr) case <-control:
cancel() default:
return
} }
select { select {
case failed <- struct{}{}: case control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}:
case <-ctx.Done(): case <-ctx.Done():
return return
} }
continue continue
} }
log.Printf("source: %v", err)
cancel()
return
}
payload = vFrame.Payload payload = vFrame.Payload
grainIdx = vFrame.Index grainIdx = vFrame.Index
if aFrame.Samples != nil && audioStream != 0 { if aFrame.Samples != nil && audioStream != 0 {
@@ -492,27 +559,20 @@ func main() {
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return return
} }
if errors.Is(err, mxl.ErrFlowInvalid) { log.Printf("source: %v", err)
log.Printf("source: flow invalid, reopening") select {
if rerr := reopen(); rerr != nil { case <-control:
log.Printf("source: reopen failed: %v", rerr) default:
cancel()
return
} }
select { select {
case failed <- struct{}{}: case control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}:
case <-ctx.Done(): case <-ctx.Done():
return return
} }
continue continue
} }
log.Printf("source: %v", err)
cancel()
return
}
payload = f.Payload payload = f.Payload
grainIdx = f.Index grainIdx = f.Index
} }
if r != nil { if r != nil {
@@ -540,11 +600,6 @@ func main() {
frameCount uint64 frameCount uint64
lastReport time.Time lastReport time.Time
lastFrame time.Time lastFrame time.Time
// ImGui stats
domainStr string = args.Domain
videoStr string = args.VideoFlowId
audioStr string = args.AudioFlowId
showStats bool = true
) )
lastFrame = time.Now() lastFrame = time.Now()
@@ -559,8 +614,13 @@ func main() {
case sdl.EventWindowResized, sdl.EventPixelSizeChanged: case sdl.EventWindowResized, sdl.EventPixelSizeChanged:
resized = true resized = true
case sdl.EventKeyDown: case sdl.EventKeyDown:
if gui.IO().WantCaptureKeyboard() {
break
}
key := *(*int32)(unsafe.Pointer(&event[28])) key := *(*int32)(unsafe.Pointer(&event[28]))
switch uint32(key) { switch uint32(key) {
case sdl.KeyQ:
fallthrough
case sdl.KeyEscape: case sdl.KeyEscape:
running = false running = false
case sdl.KeyF: case sdl.KeyF:
@@ -588,7 +648,7 @@ func main() {
} }
if !granted { if !granted {
select { select {
case grant <- struct{}{}: case control <- reconnectParams{}:
granted = true granted = true
case <-ctx.Done(): case <-ctx.Done():
running = false running = false
@@ -596,20 +656,21 @@ func main() {
} }
} }
var shownIndex uint64 var shownIndex uint64
hasFrame := false
select { select {
case shownIndex = <-staged: case shownIndex = <-staged:
granted = false granted = false
case <-failed: hasFrame = true
granted = false
continue
case <-ctx.Done(): case <-ctx.Done():
running = false running = false
continue continue
case <-time.After(100 * time.Millisecond): case <-time.After(100 * time.Millisecond):
continue // No frame staged. Reset granted so we re-grant on next iteration.
granted = false
} }
// stats // stats
if hasFrame {
if lastIndex != 0 && shownIndex > lastIndex { if lastIndex != 0 && shownIndex > lastIndex {
if g := shownIndex - lastIndex - 1; g > 0 { if g := shownIndex - lastIndex - 1; g > 0 {
dropped += g dropped += g
@@ -626,6 +687,7 @@ func main() {
dropped = 0 dropped = 0
lastReport = now lastReport = now
} }
}
// end of stats // end of stats
if r != nil { if r != nil {
gui.BeginFrame(time.Since(lastFrame), int32(r.Extent().Width), int32(r.Extent().Height)) gui.BeginFrame(time.Since(lastFrame), int32(r.Extent().Width), int32(r.Extent().Height))
@@ -633,17 +695,24 @@ func main() {
// cimgui.Begin("Test") // cimgui.Begin("Test")
if showStats { if showStats {
cimgui.SetNextWindowPos(cimgui.Vec2{X: 10, Y: 10}) cimgui.SetNextWindowPos(cimgui.Vec2{X: 10, Y: 10})
cimgui.SetNextWindowSize(cimgui.Vec2{X: 300, Y: 300}) cimgui.SetNextWindowSize(cimgui.Vec2{X: 200, Y: 200})
cimgui.BeginV("Stats", &showStats, cimgui.BeginV("Stats", &showStats,
cimgui.WindowFlagsNoTitleBar|cimgui.WindowFlagsNoBackground|cimgui.WindowFlagsNoResize|cimgui.WindowFlagsNoScrollbar) cimgui.WindowFlagsNoTitleBar|cimgui.WindowFlagsNoResize|cimgui.WindowFlagsNoScrollbar)
cimgui.Text(fmt.Sprintf("FPS: %.1f", fps)) cimgui.Text(fmt.Sprintf("FPS: %.1f", fps))
cimgui.Text(fmt.Sprintf("Dropped: %d", dropped)) cimgui.Text(fmt.Sprintf("Dropped: %d", dropped))
cimgui.Text(fmt.Sprintf("Index: %d", shownIndex)) cimgui.Text(fmt.Sprintf("Index: %d", shownIndex))
if videoSrc != nil {
cimgui.Text(fmt.Sprintf("Video: %dx%d %.2fp", videoSrc.Width(), videoSrc.Height(),
float32(videoSrc.Rate().Num/videoSrc.Rate().Den)))
}
if syncSrc != nil { if syncSrc != nil {
cimgui.Text(fmt.Sprintf("Video: %dx%d %.2fp", syncSrc.Width(), syncSrc.Height(), cimgui.Text(fmt.Sprintf("Video: %dx%d %.2fp", syncSrc.Width(), syncSrc.Height(),
float32(syncSrc.Rate().Num/syncSrc.AudioRate().Den))) float32(syncSrc.Rate().Num/syncSrc.Rate().Den)))
cimgui.Text(fmt.Sprintf("Audio: %dch %dkHz", syncSrc.Channels(), syncSrc.AudioRate().Num)) cimgui.Text(fmt.Sprintf("Audio: %dch %dkHz", syncSrc.Channels(), syncSrc.AudioRate().Num))
} }
cimgui.Text("\nPress F1 to hide stats")
cimgui.Text("Q or Esc to quit")
cimgui.Text("F for fullscreen")
cimgui.End() cimgui.End()
} }
cimgui.Begin("Connection") cimgui.Begin("Connection")
@@ -651,6 +720,9 @@ func main() {
cimgui.InputTextWithHint("Video UUID", "", &videoStr, 0, nil) cimgui.InputTextWithHint("Video UUID", "", &videoStr, 0, nil)
cimgui.InputTextWithHint("Audio UUID", "", &audioStr, 0, nil) cimgui.InputTextWithHint("Audio UUID", "", &audioStr, 0, nil)
cimgui.Checkbox("Show stats", &showStats) cimgui.Checkbox("Show stats", &showStats)
if cimgui.Button("Connect") {
doReconnect()
}
cimgui.End() cimgui.End()
gui.EndFrame() gui.EndFrame()
lastFrame = time.Now() lastFrame = time.Now()
+10 -1
View File
@@ -1,14 +1,23 @@
# Useful links
https://pthom.github.io/imgui_explorer/
# List of bugs, that need to be fixed # List of bugs, that need to be fixed
## Major ## Major
- check how it looks like with more than 2 audio channels
## Minor ## Minor
- hotkeys on keyDown, especially toggle keys. Way to recreate: press F and hold - hotkeys on keyDown, especially toggle keys. Way to recreate: press F and hold
## TODO: ## TODO:
- some sort of playlist with id's
- CLI option to run fullscreen - CLI option to run fullscreen
- fabrics bridge reader. Step by step. Start with local - fabrics bridge reader. Step by step. Start with local
- basic UI: stats, fields for domain, flow ids, label, etc. - basic UI: stats, fields for domain, flow ids, label, etc.
- snapshot - snapshot
- waveform, vectorscope - waveform, vectorscope
- some image, when audio only - some image, when audio only
- q for quit
## Done
- [x] resize broken again
- [x] q for quit
+3 -3
View File
@@ -10,11 +10,11 @@ Collapsed=0
[Window][Stats] [Window][Stats]
Pos=10,10 Pos=10,10
Size=300,300 Size=200,200
Collapsed=0 Collapsed=0
[Window][Connection] [Window][Connection]
Pos=676,497 Pos=425,351
Size=413,137 Size=523,153
Collapsed=0 Collapsed=0
+2
View File
@@ -27,8 +27,10 @@ const (
EventTextInput uint32 = 0x303 EventTextInput uint32 = 0x303
// it's about SDL_keycode, not SDL_scancode
KeyEscape uint32 = 0x1B KeyEscape uint32 = 0x1B
KeyF uint32 = 0x66 KeyF uint32 = 0x66
KeyQ uint32 = 0x71
KeyF1 uint32 = 0x4000003A KeyF1 uint32 = 0x4000003A
InitAudio uint32 = 0x00000010 InitAudio uint32 = 0x00000010
+22 -4
View File
@@ -403,6 +403,7 @@ func (s *SyncSource) Close() error {
// NextSync reads both at a synced timestamp. Returns video Frame + audio AudioFrame // NextSync reads both at a synced timestamp. Returns video Frame + audio AudioFrame
func (s *SyncSource) NextSync(ctx context.Context, audioBatch uint64, timeout time.Duration) (Frame, AudioFrame, error) { func (s *SyncSource) NextSync(ctx context.Context, audioBatch uint64, timeout time.Duration) (Frame, AudioFrame, error) {
var timeouts int
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -421,7 +422,19 @@ func (s *SyncSource) NextSync(ctx context.Context, audioBatch uint64, timeout ti
s.idx = mxl.CurrentIndex(s.rate) s.idx = mxl.CurrentIndex(s.rate)
continue continue
} }
return Frame{}, AudioFrame{}, fmt.Errorf("GetGraing: %w", gerr) if errors.Is(gerr, mxl.ErrOutOfRangeLate) {
s.idx = mxl.CurrentIndex(s.rate)
continue
}
if errors.Is(gerr, mxl.ErrOutOfRangeEarly) {
select {
case <-time.After(5 * time.Millisecond):
case <-ctx.Done():
return Frame{}, AudioFrame{}, ctx.Err()
}
continue
}
return Frame{}, AudioFrame{}, fmt.Errorf("GetGrain: %w", gerr)
} }
// read audio at the same timestamp // read audio at the same timestamp
aIdx := mxl.TimestampToIndex(s.aRate, ts) aIdx := mxl.TimestampToIndex(s.aRate, ts)
@@ -451,14 +464,19 @@ func (s *SyncSource) NextSync(ctx context.Context, audioBatch uint64, timeout ti
} }
// even if audio failed, video returns // even if audio failed, video returns
return vFrame, aFrame, nil return vFrame, aFrame, nil
case errors.Is(err, mxl.ErrTimeout), errors.Is(err, mxl.ErrOutOfRangeEarly): case errors.Is(err, mxl.ErrTimeout), errors.Is(err, mxl.ErrOutOfRangeEarly), errors.Is(err, mxl.ErrOutOfRangeLate):
timeouts++
if timeouts > 10 {
timeouts = 0
s.idx = mxl.CurrentIndex(s.rate)
return Frame{}, AudioFrame{}, fmt.Errorf("sync: feeds not responding")
}
s.idx = mxl.CurrentIndex(s.rate)
select { select {
case <-time.After(5 * time.Millisecond): case <-time.After(5 * time.Millisecond):
case <-ctx.Done(): case <-ctx.Done():
return Frame{}, AudioFrame{}, ctx.Err() return Frame{}, AudioFrame{}, ctx.Err()
} }
case errors.Is(err, mxl.ErrOutOfRangeLate):
s.idx = mxl.CurrentIndex(s.rate)
default: default:
return Frame{}, AudioFrame{}, fmt.Errorf("WaitForDataAt: %w", err) return Frame{}, AudioFrame{}, fmt.Errorf("WaitForDataAt: %w", err)
} }
+15
View File
@@ -0,0 +1,15 @@
[Window][Debug##Default]
Pos=60,60
Size=400,400
Collapsed=0
[Window][Stats]
Pos=10,10
Size=200,200
Collapsed=0
[Window][Connection]
Pos=60,60
Size=110,146
Collapsed=0
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
VIDEO_ID="5fbec3b1-1b0f-417d-9059-8b94a47197ef"
AUDIO_ID="5fbec3b1-1b0f-417d-9059-8b94a47197eb"
VIDEO_URI=$1
if [[ -z "${VIDEO_URI}" ]] then
VIDEO_URI="${HOME}/Videos/test-vid/f1.ts"
fi
export GST_PLUGIN_PATH="${HOME}/.gst-plugin:${GST_PLUGIN_PATH}"
mxl-gst-looping-filesrc -d /dev/shm/mxl -i "${VIDEO_URI}" --video-id "${VIDEO_ID}" --audio-id "${AUDIO_ID}" 2>/dev/null
# sleep 5
# kill -9 $(pidof "mxl-gst-looping-filesrc")
# echo -e "\ngst-looping-filesrc killed"
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
#!/bin/bash
VIDEO_ID="5fbec3b1-1b0f-417d-9059-8b94a47197ef"
AUDIO_ID="5fbec3b1-1b0f-417d-9059-8b94a47197eb"
VIDEO_URI=$1
if [[ -z "${VIDEO_URI}" ]] then
VIDEO_URI="${HOME}/Videos/test-vid/f1.ts"
fi
export GST_PLUGIN_PATH="${HOME}/.gst-plugin:${GST_PLUGIN_PATH}"
mxl-gst-looping-filesrc -d /dev/shm/mxl -i "${VIDEO_URI}" --video-id "${VIDEO_ID}" --audio-id "${AUDIO_ID}" 2>/dev/null &
sleep 1
echo "Writer has been started"
go run ../cmd/mxl-player -d /dev/shm/mxl -v "${VIDEO_ID}" -a "${AUDIO_ID}" &> /tmp/player.log &
echo "mxl-player has been started"
sleep 5
pkill mxl-gst
echo "Writer stopped" >> /tmp/player.log
sleep 5
echo "Writer has been started again" >> /tmp/player.log
mxl-gst-looping-filesrc -d /dev/shm/mxl -i "${VIDEO_URI}" --video-id "${VIDEO_ID}" --audio-id "${AUDIO_ID}" 2>/dev/null
Executable
BIN
View File
Binary file not shown.