"""GStreamer pipeline builder for audio/video streaming. Constructs GStreamer pipeline strings for the Raspberry Pi 4B hardware-accelerated encoding path: Video: camera → h264 encoder → RTMP mux Audio: JACK/ALSA → AAC encoder → RTMP mux Supports both Python GObject bindings (preferred) and CLI gst-launch-1.0 subprocess mode for reliability. Raspberry Pi 4B hardware encoders: - v4l2h264enc: V4L2 stateless hardware h264 encoder (Pi 4/5) - omxh264enc: Legacy Broadcom MMAL hardware encoder (Pi 3/older) - x264enc: Software fallback (works everywhere, higher CPU) Audio capture methods: - alsasrc: Direct ALSA device (e.g., JACK ALSA bridge) - pulsesrc: PulseAudio source - jackaudiosrc: JACK audio source (requires gst-plugins-bad) """ from __future__ import annotations import logging from dataclasses import dataclass, field from enum import StrEnum from typing import Optional logger = logging.getLogger(__name__) class VideoSource(StrEnum): """Video input source type for GStreamer pipeline.""" V4L2 = "v4l2" # USB webcam /dev/video* LIBCAMERA = "libcamera" # Pi Camera Module (modern libcamera) RASPICAM = "raspicam" # Legacy Pi Camera (rpicamsrc) TEST = "test" # Test pattern (videotestsrc) class AudioSource(StrEnum): """Audio input source type for GStreamer pipeline.""" ALSA = "alsa" # Direct ALSA device JACK = "jack" # JACK audio server PULSE = "pulse" # PulseAudio TEST = "test" # Test tone (audiotestsrc) class EncoderType(StrEnum): """Video encoder type.""" V4L2_H264 = "v4l2h264enc" # V4L2 stateless h264 (Pi 4/5) OMX_H264 = "omxh264enc" # Broadcom OMX h264 (Pi 3/older) X264 = "x264enc" # Software x264 LIBCAMERA_H264 = "libcamera_h264" # libcamera built-in h264 @dataclass class GstPipelineConfig: """Configuration for building a GStreamer pipeline.""" # Video video_source: VideoSource = VideoSource.TEST video_device: str = "/dev/video0" video_width: int = 1280 video_height: int = 720 video_framerate: int = 30 # Audio audio_source: AudioSource = AudioSource.TEST audio_device: str = "hw:0" # ALSA device name audio_sample_rate: int = 48000 audio_channels: int = 2 audio_buffer_time_us: int = 20000 # 20ms buffer for low latency # Encoding video_encoder: EncoderType = EncoderType.V4L2_H264 video_bitrate_kbps: int = 2500 video_bitrate_max_kbps: int = 4000 keyframe_interval: int = 60 # frames between keyframes h264_profile: str = "main" h264_level: str = "4.0" b_frames: int = 0 # 0 for lowest latency audio_bitrate_kbps: int = 128 audio_encoder: str = "avenc_aac" # or voaacenc, lamemp3enc # Output rtmp_url: str = "" rtmp_stream_key: str = "" # Latency tuning low_latency: bool = True video_queue_max_time_ms: int = 1000 zerolatency_tune: bool = True # Test mode test_video_pattern: int = 0 # videotestsrc pattern number test_audio_freq: float = 440.0 # audiotestsrc frequency @property def full_rtmp_url(self) -> str: """Construct full RTMP URL with stream key.""" if not self.rtmp_url: return "" url = self.rtmp_url.rstrip("/") if self.rtmp_stream_key: url = f"{url}/{self.rtmp_stream_key}" return url class GstPipelineBuilder: """Builds GStreamer pipeline strings for various configurations. Usage: builder = GstPipelineBuilder(config) pipeline_str = builder.build() # Or for CLI mode: subprocess.run(["gst-launch-1.0", "-e"] + builder.build_args()) """ def __init__(self, config: GstPipelineConfig): self.config = config def build(self) -> str: """Build a complete GStreamer pipeline string.""" elements = self._build_elements() return " ! ".join(elements) def build_args(self) -> list[str]: """Build GStreamer pipeline as argument list for subprocess.""" return self.build().split(" ! ") def _build_elements(self) -> list[str]: """Build all pipeline element strings.""" elements: list[str] = [] # Video branch video_elements = self._build_video_branch() # Audio branch audio_elements = self._build_audio_branch() # Combine with flvmux + rtmpsink elements.append( f"flvmux name=mux streamable=true " f"{' ! rtmpsink location=' + self.config.full_rtmp_url + ' sync=false' if self.config.full_rtmp_url else ''}" ) # Build the full pipeline string with named branches video_str = " ! ".join(video_elements) audio_str = " ! ".join(audio_elements) # Return GStreamer pipeline with named pads flvmux_elements = ["flvmux", "name=mux", "streamable=true"] if self.config.full_rtmp_url: flvmux_elements.append(f"rtmpsink location={self.config.full_rtmp_url} sync=false") else: flvmux_elements.append("fakesink sync=false") mux_str = " ! ".join(flvmux_elements) full = f"{video_str} ! mux.video {audio_str} ! mux.audio {mux_str}" return full.split(" ! ") def _build_video_branch(self) -> list[str]: """Build the video source + encoding branch.""" elements: list[str] = [] cfg = self.config # Video source source = self._build_video_source() elements.append(source) # Video processing elements.append("videoconvert") elements.append(f"video/x-raw,format=I420,width={cfg.video_width},height={cfg.video_height},framerate={cfg.video_framerate}/1") # Queue for buffering if cfg.low_latency: elements.append(f"queue max-size-time={cfg.video_queue_max_time_ms * 1000000} max-size-buffers=0 max-size-bytes=0") else: elements.append("queue") # Video encoder encoder = self._build_video_encoder() elements.extend(encoder) # H.264 parsing elements.append("h264parse") elements.append(f"video/x-h264,profile={cfg.h264_profile},level={cfg.h264_level}") return elements def _build_video_source(self) -> str: """Build the video source element string.""" cfg = self.config if cfg.video_source == VideoSource.V4L2: return f"v4l2src device={cfg.video_device} do-timestamp=true" elif cfg.video_source == VideoSource.LIBCAMERA: # libcamerasrc — modern Pi camera stack return ( f"libcamerasrc camera-name={cfg.video_device} " f"! video/x-raw,width={cfg.video_width},height={cfg.video_height},framerate={cfg.video_framerate}/1" ) elif cfg.video_source == VideoSource.RASPICAM: # rpicamsrc — legacy Pi camera stack return ( f"rpicamsrc preview=false bitrate={cfg.video_bitrate_kbps * 1000} " f"! video/x-h264,width={cfg.video_width},height={cfg.video_height},framerate={cfg.video_framerate}/1,profile={cfg.h264_profile}" ) elif cfg.video_source == VideoSource.TEST: return f"videotestsrc pattern={cfg.test_video_pattern} is-live=true" return f"v4l2src device={cfg.video_device}" def _build_video_encoder(self) -> list[str]: """Build the video encoder element(s).""" cfg = self.config if cfg.video_encoder == EncoderType.LIBCAMERA_H264: # libcamera does its own encoding, skip external encoder return [] if cfg.video_encoder == EncoderType.V4L2_H264: return [ f"v4l2h264enc " f"extra-controls='controls," f"video_bitrate={cfg.video_bitrate_kbps * 1000}," f"repeat_sequence_header=1," f"h264_profile=4," # main profile f"h264_level=10," # level 4.0 f"video_bitrate_mode=0'" # VBR ] elif cfg.video_encoder == EncoderType.OMX_H264: return [ f"omxh264enc " f"target-bitrate={cfg.video_bitrate_kbps * 1000} " f"control-rate=variable " f"periodicity-idr={cfg.keyframe_interval} " f"inline-header=true" ] elif cfg.video_encoder == EncoderType.X264: tune = "zerolatency" if cfg.low_latency else "film" speed_preset = "ultrafast" if cfg.low_latency else "veryfast" return [ f"x264enc " f"bitrate={cfg.video_bitrate_kbps} " f"speed-preset={speed_preset} " f"tune={tune} " f"key-int-max={cfg.keyframe_interval} " f"bframes={cfg.b_frames} " f"byte-stream=true" ] return ["x264enc"] def _build_audio_branch(self) -> list[str]: """Build the audio source + encoding branch.""" elements: list[str] = [] cfg = self.config # Audio source source = self._build_audio_source() elements.append(source) # Audio conversion elements.append("audioconvert") if cfg.audio_channels == 1: elements.append("audio/x-raw,channels=1") elements.append("audioresample") elements.append(f"audio/x-raw,rate={cfg.audio_sample_rate}") # Queue for buffering if cfg.low_latency: elements.append(f"queue max-size-time={cfg.video_queue_max_time_ms * 1000000} max-size-buffers=0 max-size-bytes=0") else: elements.append("queue") # Audio encoder audio_enc = self._build_audio_encoder() elements.append(audio_enc) return elements def _build_audio_source(self) -> str: """Build the audio source element string.""" cfg = self.config if cfg.audio_source == AudioSource.ALSA: return ( f"alsasrc device={cfg.audio_device} " f"buffer-time={cfg.audio_buffer_time_us} " f"do-timestamp=true" ) elif cfg.audio_source == AudioSource.JACK: # jackaudiosrc — requires gst-plugins-bad return "jackaudiosrc connect=0" elif cfg.audio_source == AudioSource.PULSE: return "pulsesrc do-timestamp=true" elif cfg.audio_source == AudioSource.TEST: return f"audiotestsrc freq={cfg.test_audio_freq} is-live=true" return "alsasrc" def _build_audio_encoder(self) -> str: """Build the audio encoder element string.""" cfg = self.config return f"{cfg.audio_encoder} bitrate={cfg.audio_bitrate_kbps * 1000}" def build_pipeline_string( video_device: str = "/dev/video0", video_width: int = 1280, video_height: int = 720, video_framerate: int = 30, video_bitrate_kbps: int = 2500, audio_device: str = "hw:0", audio_sample_rate: int = 48000, audio_bitrate_kbps: int = 128, rtmp_url: str = "", low_latency: bool = True, ) -> str: """Quick helper to build a streaming pipeline string. This is a convenience function that creates a default configuration and returns the GStreamer pipeline string. For full control, use GstPipelineBuilder with a custom GstPipelineConfig. Args: video_device: Path to video device (e.g. /dev/video0). video_width: Output video width. video_height: Output video height. video_framerate: Output framerate. video_bitrate_kbps: Video encoder bitrate in kbps. audio_device: ALSA device name (e.g. hw:0). audio_sample_rate: Audio sample rate in Hz. audio_bitrate_kbps: Audio encoder bitrate in kbps. rtmp_url: Full RTMP URL including stream key. low_latency: Enable zero-latency tuning. Returns: GStreamer pipeline string suitable for gst-launch-1.0. """ config = GstPipelineConfig( video_source=VideoSource.V4L2 if video_device != "test" else VideoSource.TEST, video_device=video_device, video_width=video_width, video_height=video_height, video_framerate=video_framerate, video_bitrate_kbps=video_bitrate_kbps, audio_source=AudioSource.ALSA if audio_device != "test" else AudioSource.TEST, audio_device=audio_device, audio_sample_rate=audio_sample_rate, audio_bitrate_kbps=audio_bitrate_kbps, rtmp_url=rtmp_url, low_latency=low_latency, ) builder = GstPipelineBuilder(config) return builder.build()