Add an audio source picker to the GUI so users can capture a specific PipeWire device (sink monitor or microphone) instead of only the system default output. - vendor go2tv.app/screencast and patch the audio stream to accept a target PipeWire node serial (PW_KEY_TARGET_OBJECT); the upstream lib only ever auto-connected to the default - capture: ListAudioSources enumerates PipeWire sinks/sources via pw-dump; OpenPipeWire takes the selected node serial - flinger/config/gui: AudioSource config field, persisted and exposed as an Audio source dropdown (default output + enumerated devices) Also fixes two pre-existing bugs surfaced by stop/start testing: - engine Stop now waits for the video/audio/stats goroutines before destroying the encoder (was a use-after-free SIGSEGV) - Start/Stop now pause/resume the engine instead of tearing down and re-opening the portal session, which the portal cannot reliably do in-process (2nd CreateSession returned Ended/cancelled). Capture session stays open across stop/start.
87 lines
2.5 KiB
Go
87 lines
2.5 KiB
Go
// 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
|
|
}
|