Skip to content

Commit e9a395a

Browse files
committed
Fix format when using software encoders
1 parent 161cacd commit e9a395a

1 file changed

Lines changed: 16 additions & 11 deletions

File tree

internal/airplay/capture.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
423426
type 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

Comments
 (0)