// Package capture provides the PipeWire backend for Wayland screen + audio // capture. // // This uses go2tv.app/screencast (MIT) which implements the full // xdg-desktop-portal ScreenCast negotiation and then receives frames over // PipeWire. The portal session is responsible for the screen-share consent // dialog presented by the compositor (Hyprland in our dev environment). // // Frames arrive as raw BGRA at the monitor's native resolution/refresh rate. // Audio is signed 16-bit, 48 kHz, stereo interleaved PCM from the system's // default output. package capture import ( "errors" "io" "go2tv.app/screencast/capture" ) // PipeWire implements Capture on top of the screencast library. type PipeWire struct { stream *capture.Stream } // OpenPipeWire opens a PipeWire capture session. streamIndex selects which // monitor to capture when multiple are present; audioSourceSerial optionally // selects a specific PipeWire audio node (0 = system default). Triggering the // portal consent dialog is expected; the compositor decides whether to show it. func OpenPipeWire(streamIndex int, audio bool, audioSourceSerial uint64) (*PipeWire, error) { s, err := capture.Open(&capture.Options{ StreamIndex: streamIndex, IncludeAudio: audio, AudioSourceSerial: audioSourceSerial, }) if err != nil { return nil, err } return &PipeWire{stream: s}, nil } // Video returns the BGRA frame source. func (p *PipeWire) Video() FrameSource { return &pipewireVideo{stream: p.stream} } // Audio returns the system audio source, or nil if unavailable. func (p *PipeWire) Audio() AudioSource { if p.stream.Audio == nil { return nil } return p.stream.Audio } // Close releases the capture session and portal resources. func (p *PipeWire) Close() error { return p.stream.Close() } // pipewireVideo adapts the screencast io.ReadCloser into FrameSource. type pipewireVideo struct { stream *capture.Stream frame *VideoFrame } // NextFrame blocks until the next full frame is delivered. The screencast // library writes one complete BGRA frame per Read, so we assemble it with // ReadFull and reuse the underlying buffer across calls. func (v *pipewireVideo) NextFrame() (*VideoFrame, error) { w, h := int(v.stream.Width), int(v.stream.Height) if v.frame == nil { v.frame = &VideoFrame{ Pix: make([]byte, w*h*4), Width: w, Height: h, Stride: w * 4, } } if _, err := io.ReadFull(v.stream, v.frame.Pix); err != nil { if errors.Is(err, io.EOF) { return nil, io.ErrClosedPipe } return nil, err } return v.frame, nil }