From 0a0d04f3a7afee239f91241f31db627e29b47260 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 7 Jun 2026 22:30:12 +0800 Subject: [PATCH] fix: normalize desktop capture dimensions --- internal/desktop/pipeline.go | 50 +++++++++++++++++++++---------- internal/desktop/pipeline_test.go | 14 +++++++++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/internal/desktop/pipeline.go b/internal/desktop/pipeline.go index a50448b..60f5bc7 100644 --- a/internal/desktop/pipeline.go +++ b/internal/desktop/pipeline.go @@ -20,6 +20,32 @@ func normalizeRTPPort(port int) int { return port } +func normalizeVideoDimension(value, fallback int) int { + if value <= 0 { + value = fallback + } + if value%2 != 0 { + value-- + } + if value < 2 { + return fallback + } + return value +} + +func normalizePipelineConfig(cfg PipelineConfig) PipelineConfig { + cfg.Port = normalizeRTPPort(cfg.Port) + cfg.Width = normalizeVideoDimension(cfg.Width, 1280) + cfg.Height = normalizeVideoDimension(cfg.Height, 720) + if cfg.FPS <= 0 { + cfg.FPS = 30 + } + if cfg.Bitrate <= 0 { + cfg.Bitrate = 2000 + } + return cfg +} + // PipelineManager manages the screen capture process lifecycle type PipelineManager struct { cmd *exec.Cmd @@ -58,19 +84,7 @@ func (pm *PipelineManager) Start(cfg PipelineConfig) error { cfg.Display = ":0.0" } } - cfg.Port = normalizeRTPPort(cfg.Port) - if cfg.Width <= 0 { - cfg.Width = 1280 - } - if cfg.Height <= 0 { - cfg.Height = 720 - } - if cfg.FPS <= 0 { - cfg.FPS = 30 - } - if cfg.Bitrate <= 0 { - cfg.Bitrate = 2000 - } + cfg = normalizePipelineConfig(cfg) tool, args, err := pm.resolvePipeline(cfg) if err != nil { @@ -172,13 +186,18 @@ func (pm *PipelineManager) hasExecutable(name string) bool { } func (pm *PipelineManager) buildGStreamer(cfg PipelineConfig) (string, []string, error) { + cfg = normalizePipelineConfig(cfg) var pipelineParts []string // 1. Capture Source (X11) pipelineParts = append(pipelineParts, fmt.Sprintf("ximagesrc display-name=%s", cfg.Display)) - pipelineParts = append(pipelineParts, "video/x-raw,framerate=30/1") + pipelineParts = append(pipelineParts, fmt.Sprintf("video/x-raw,framerate=%d/1", cfg.FPS)) pipelineParts = append(pipelineParts, "videoconvert") - pipelineParts = append(pipelineParts, "video/x-raw,format=I420") + pipelineParts = append(pipelineParts, "videoscale") + pipelineParts = append( + pipelineParts, + fmt.Sprintf("video/x-raw,format=I420,width=%d,height=%d,framerate=%d/1", cfg.Width, cfg.Height, cfg.FPS), + ) // 2. Encoder encoderStr := "x264enc speed-preset=ultrafast tune=zerolatency bitrate=" + fmt.Sprintf("%d", cfg.Bitrate) + " byte-stream=true key-int-max=30" @@ -211,6 +230,7 @@ func (pm *PipelineManager) buildGStreamer(cfg PipelineConfig) (string, []string, } func (pm *PipelineManager) buildFFmpeg(cfg PipelineConfig) (string, []string, error) { + cfg = normalizePipelineConfig(cfg) args := []string{ "-f", "x11grab", "-draw_mouse", "1", diff --git a/internal/desktop/pipeline_test.go b/internal/desktop/pipeline_test.go index 410d633..8fc37c8 100644 --- a/internal/desktop/pipeline_test.go +++ b/internal/desktop/pipeline_test.go @@ -23,6 +23,8 @@ func TestBuildGStreamerUsesBrowserFriendlyH264(t *testing.T) { joined := strings.Join(args, " ") for _, expected := range []string{ "video/x-raw,format=I420", + "width=1280", + "height=720", "x264enc", "tune=zerolatency", "key-int-max=30", @@ -37,6 +39,18 @@ func TestBuildGStreamerUsesBrowserFriendlyH264(t *testing.T) { } } +func TestNormalizeVideoDimensionUsesEvenValues(t *testing.T) { + if got := normalizeVideoDimension(1353, 1280); got != 1352 { + t.Fatalf("expected odd dimension to be rounded down to 1352, got %d", got) + } + if got := normalizeVideoDimension(0, 720); got != 720 { + t.Fatalf("expected fallback for zero dimension, got %d", got) + } + if got := normalizeVideoDimension(1, 720); got != 720 { + t.Fatalf("expected fallback for too-small dimension, got %d", got) + } +} + func TestBuildFFmpegUsesBrowserFriendlyH264(t *testing.T) { pm := NewPipelineManager()