@@ -103,8 +103,10 @@ func startWaylandCapture(ctx context.Context, cfg CaptureConfig) (*ScreenCapture
103103 // H.264.
104104 // - vapostproc imports the portal's DMA-BUF via VA-API when available
105105 // Systems without VA-API (such as Asahi Linux) fall back to videoconvert.
106- // - format=I420 forces 4:2:0 — RGB screens otherwise make x264enc emit
107- // "High 4:4:4 Predictive", which most receiver decoders reject (black).
106+ // - The raw format is pinned to the encoder's required 4:2:0 format. The
107+ // hardware encoders require NV12, while the software encoders use I420.
108+ // Pinning the format also prevents x264enc from emitting High 4:4:4
109+ // Predictive, which most receiver decoders reject (black).
108110 // - videorate re-stamps buffers onto a regular fps timeline: the portal can
109111 // deliver pts=0, which confuses encoder/muxer timing. drop-only=true never
110112 // duplicates frames during idle periods (no wasted bandwidth on a static
@@ -127,11 +129,11 @@ func startWaylandCapture(ctx context.Context, cfg CaptureConfig) (*ScreenCapture
127129 }
128130
129131 gstArgs = append (gstArgs ,
130- "!" , "videoconvert" ,
131- "!" , "video/x-raw,format=I420" ,
132- "!" , "videorate" , "drop-only=true" , "skip-to-first=true" ,
133- "!" , fmt .Sprintf ("video/x-raw,framerate=%d/1" , fps ),
134- "!" , "queue" , "max-size-buffers=1" , "max-size-bytes=0" , "max-size-time=0" , "leaky=downstream" ,
132+ "!" , "videoconvert" ,
133+ "!" , fmt . Sprintf ( "video/x-raw,format=%s" , encoderParts . rawFormat ) ,
134+ "!" , "videorate" , "drop-only=true" , "skip-to-first=true" ,
135+ "!" , fmt .Sprintf ("video/x-raw,framerate=%d/1" , fps ),
136+ "!" , "queue" , "max-size-buffers=1" , "max-size-bytes=0" , "max-size-time=0" , "leaky=downstream" ,
135137 )
136138 if encoderParts .needsVulkan {
137139 gstArgs = append (gstArgs , "!" , "vulkanupload" )
@@ -235,6 +237,7 @@ func startX11Capture(ctx context.Context, cfg CaptureConfig) (*ScreenCapture, er
235237 "!" , fmt .Sprintf ("video/x-raw,framerate=%d/1" , fps ),
236238 "!" , "queue" , "max-size-buffers=1" , "max-size-bytes=0" , "max-size-time=0" , "leaky=downstream" ,
237239 "!" , "videoconvert" ,
240+ "!" , fmt .Sprintf ("video/x-raw,format=%s" , encoder .rawFormat ),
238241 )
239242 if encoder .needsVulkan {
240243 gstArgs = append (gstArgs , "!" , "vulkanupload" )
@@ -422,7 +425,8 @@ func parseXrandrGeometry(line string) (xOffset, yOffset, width, height int, ok b
422425// a vulkanupload step before the encoder.
423426type encoderResult struct {
424427 parts []string
425- needsVulkan bool // encoder needs vulkanupload ! before it
428+ needsVulkan bool // encoder needs vulkanupload ! before it
429+ rawFormat string // system-memory format produced by videoconvert
426430}
427431
428432// detectGstEncoder probes for available GStreamer H.264 encoders and returns
@@ -450,6 +454,7 @@ func detectGstEncoder(cfg CaptureConfig) encoderResult {
450454 fmt .Sprintf ("bitrate=%d" , bitrate ),
451455 },
452456 needsVulkan : true ,
457+ rawFormat : "NV12" ,
453458 }
454459 }
455460 }
@@ -458,7 +463,7 @@ func detectGstEncoder(cfg CaptureConfig) encoderResult {
458463 if hwaccel == "auto" || hwaccel == "nvenc" {
459464 if exec .Command ("gst-inspect-1.0" , "nvh264enc" ).Run () == nil {
460465 log .Printf ("[CAPTURE] using NVENC hardware encoding (nvh264enc)" )
461- return encoderResult {parts : []string {
466+ return encoderResult {rawFormat : "NV12" , parts : []string {
462467 "nvh264enc" ,
463468 fmt .Sprintf ("bitrate=%d" , bitrate ),
464469 fmt .Sprintf ("gop-size=%d" , keyframeInterval ),
@@ -477,7 +482,7 @@ func detectGstEncoder(cfg CaptureConfig) encoderResult {
477482 if hwaccel == "auto" || hwaccel == "vaapi" {
478483 if exec .Command ("gst-inspect-1.0" , "vah264enc" ).Run () == nil {
479484 log .Printf ("[CAPTURE] using VAAPI hardware encoding (vah264enc)" )
480- return encoderResult {parts : []string {
485+ return encoderResult {rawFormat : "NV12" , parts : []string {
481486 "vah264enc" ,
482487 fmt .Sprintf ("bitrate=%d" , bitrate ),
483488 fmt .Sprintf ("key-int-max=%d" , keyframeInterval ),
@@ -496,7 +501,7 @@ func detectGstEncoder(cfg CaptureConfig) encoderResult {
496501 // Use VBR (pass=0) so the encoder can undershoot on simple scenes, saving
497502 // headroom for complex frames. vbv-buf-capacity + vbv-maxrate cap bursts.
498503 maxrate := bitrate + bitrate / 4 // allow 25% overshoot on peaks
499- return encoderResult {parts : []string {
504+ return encoderResult {rawFormat : "I420" , parts : []string {
500505 "x264enc" ,
501506 "tune=zerolatency" ,
502507 "speed-preset=superfast" ,
0 commit comments