Skip to main content

nv_media/
gpu_provider.rs

1//! GPU pipeline provider — extension point for platform-specific GPU residency.
2//!
3//! [`GpuPipelineProvider`] is the public trait that platform-specific crates
4//! implement to provide GPU-resident frame delivery through GStreamer.
5//! The built-in CUDA path (via `DeviceResidency::Cuda`) uses upstream
6//! GStreamer CUDA elements (`cudaupload`, `cudaconvert`), available on
7//! GStreamer >= 1.20 (including JetPack 6 / L4T R36).  For older
8//! platforms (e.g., JetPack 5.x), external crates like `nv-jetson`
9//! implement this trait to provide an alternative GPU memory path.
10//!
11//! External crates (e.g., `nv-jetson`) implement this trait to support
12//! hardware where the upstream CUDA elements are not available — for
13//! example, NVMM-based GPU residency on JetPack 5.x (GStreamer 1.16).
14//!
15//! # GStreamer dependency
16//!
17//! This trait takes GStreamer types (`gstreamer::Sample`) in its method
18//! signatures because it operates at the media backend boundary. External
19//! crates implementing this trait explicitly opt into the `gstreamer`
20//! dependency. This is the deliberate extension surface for the GStreamer
21//! backend — upstream of this point, no GStreamer types are visible.
22//!
23//! # Pipeline topology
24//!
25//! The provider controls two parts of the per-feed pipeline:
26//!
27//! 1. **Pipeline tail** — the GStreamer elements between the decoder and
28//!    the appsink (`build_pipeline_tail`). For upstream CUDA this is
29//!    `cudaupload → cudaconvert → appsink(CUDAMemory)`. For Jetson NVMM
30//!    it might be `nvvidconv → appsink(NVMM)` or just `appsink(NVMM)`.
31//!
32//! 2. **Frame bridge** — the function that converts a `GstSample` into a
33//!    `FrameEnvelope` with device-resident
34//!    pixel data (`bridge_sample`).
35//!
36//! The provider controls the full pipeline tail; any decoder-to-tail
37//! bridging elements should be included as the first element(s) in
38//! the tail returned by `build_pipeline_tail`.
39
40use std::sync::Arc;
41#[cfg(feature = "gst-backend")]
42use std::sync::atomic::AtomicU64;
43
44#[cfg(feature = "gst-backend")]
45use nv_core::error::MediaError;
46#[cfg(feature = "gst-backend")]
47use nv_core::id::FeedId;
48#[cfg(feature = "gst-backend")]
49use nv_frame::FrameEnvelope;
50#[cfg(feature = "gst-backend")]
51use nv_frame::frame::PixelFormat;
52
53#[cfg(feature = "gst-backend")]
54use crate::bridge::PtzTelemetry;
55
56/// Result of [`GpuPipelineProvider::build_pipeline_tail`].
57///
58/// Contains the GStreamer elements that form the pipeline segment between
59/// the decoder (or post-decode hook) and the appsink, plus the configured
60/// appsink itself.
61#[cfg(feature = "gst-backend")]
62pub struct GpuPipelineTail {
63    /// Ordered converter elements to insert before the appsink.
64    ///
65    /// May be empty if the decoder output negotiates directly with the
66    /// appsink (e.g., NVMM passthrough).  Elements are linked in order:
67    /// `elements[0] → elements[1] → ... → appsink`.
68    pub elements: Vec<gstreamer::Element>,
69    /// The configured appsink with appropriate caps set.
70    pub appsink: gstreamer_app::AppSink,
71}
72
73/// Extension point for GPU-resident pipeline construction.
74///
75/// Platform-specific crates implement this trait to provide tailored
76/// pipeline topology and frame bridging for their GPU memory model.
77///
78/// The built-in CUDA path is available via `DeviceResidency::Cuda`
79/// without implementing this trait.  Applications only need a custom
80/// provider when the built-in elements are unavailable (e.g., NVMM
81/// on JetPack 5.x) or when a different GPU memory model is required.
82///
83/// # Thread safety
84///
85/// Implementations must be `Send + Sync` because the provider is shared
86/// between the pipeline-building code (source management thread) and the
87/// appsink callback (GStreamer streaming thread) via `Arc`.
88///
89/// # Example
90///
91/// ```rust,ignore
92/// use std::sync::Arc;
93/// use nv_media::gpu_provider::GpuPipelineProvider;
94/// use nv_media::DeviceResidency;
95///
96/// let provider: Arc<dyn GpuPipelineProvider> = Arc::new(MyJetsonProvider::new());
97///
98/// let config = FeedConfig::builder()
99///     .device_residency(DeviceResidency::Provider(provider))
100///     // ...
101///     .build()?;
102/// ```
103pub trait GpuPipelineProvider: Send + Sync {
104    /// Human-readable name for logging and diagnostics.
105    fn name(&self) -> &str;
106
107    /// Build the GPU pipeline tail (converter elements + appsink).
108    ///
109    /// Called once per session during pipeline construction. The returned
110    /// elements are added to the pipeline and linked in order, followed
111    /// by the appsink.
112    ///
113    /// # Arguments
114    ///
115    /// * `pixel_format` — the target pixel format for the appsink caps.
116    ///
117    /// # Errors
118    ///
119    /// Return `MediaError::Unsupported` if the required GStreamer elements
120    /// or capabilities are not available at runtime.
121    #[cfg(feature = "gst-backend")]
122    fn build_pipeline_tail(&self, pixel_format: PixelFormat)
123    -> Result<GpuPipelineTail, MediaError>;
124
125    /// Bridge a GStreamer sample into a device-resident [`FrameEnvelope`].
126    ///
127    /// Called on every frame from the appsink streaming thread. Must be
128    /// efficient — allocations and blocking should be minimized.
129    ///
130    /// The returned `FrameEnvelope` should carry `PixelData::Device` with
131    /// a platform-specific handle (e.g., `CudaBufferHandle` or
132    /// `NvmmBufferHandle`) and optionally a `HostMaterializeFn` for
133    /// transparent CPU fallback.
134    #[cfg(feature = "gst-backend")]
135    fn bridge_sample(
136        &self,
137        feed_id: FeedId,
138        seq: &Arc<AtomicU64>,
139        pixel_format: PixelFormat,
140        sample: &gstreamer::Sample,
141        ptz: Option<PtzTelemetry>,
142    ) -> Result<FrameEnvelope, MediaError>;
143}
144
145/// Shared handle to a [`GpuPipelineProvider`].
146///
147/// Used by `IngressOptions`,
148/// `SessionConfig`, and the pipeline
149/// builder.
150pub type SharedGpuProvider = Arc<dyn GpuPipelineProvider>;
151
152// ---------------------------------------------------------------------------
153// Provider authoring helpers
154// ---------------------------------------------------------------------------
155
156/// Pre-extracted metadata from a GStreamer sample.
157///
158/// Providers call [`SampleInfo::extract()`] at the top of their
159/// [`bridge_sample`](GpuPipelineProvider::bridge_sample) implementation
160/// to avoid re-deriving width/height/stride/timestamps from raw
161/// GStreamer types.
162///
163/// # Example
164///
165/// ```rust,ignore
166/// fn bridge_sample(
167///     &self,
168///     feed_id: FeedId,
169///     seq: &Arc<AtomicU64>,
170///     pixel_format: PixelFormat,
171///     sample: &gstreamer::Sample,
172///     ptz: Option<PtzTelemetry>,
173/// ) -> Result<FrameEnvelope, MediaError> {
174///     let info = SampleInfo::extract(sample, seq)?;
175///     // … platform-specific handle extraction …
176///     Ok(info.into_device_envelope(feed_id, pixel_format, handle, Some(materialize), ptz))
177/// }
178/// ```
179#[cfg(feature = "gst-backend")]
180pub struct SampleInfo {
181    /// Frame width in pixels.
182    pub width: u32,
183    /// Frame height in pixels.
184    pub height: u32,
185    /// Row stride in bytes (from the first video plane).
186    pub stride: u32,
187    /// Monotonic presentation timestamp.
188    pub ts: nv_core::timestamp::MonotonicTs,
189    /// Wall-clock timestamp captured at extraction time.
190    pub wall_ts: nv_core::timestamp::WallTs,
191    /// Monotonically increasing frame sequence number.
192    pub frame_seq: u64,
193    /// Owned copy of the GStreamer buffer (ref-count bump, not a data copy).
194    pub buffer: gstreamer::Buffer,
195}
196
197#[cfg(feature = "gst-backend")]
198impl SampleInfo {
199    /// Extract common metadata from a GStreamer sample.
200    ///
201    /// Parses `VideoInfo` from the sample caps, reads the PTS, bumps
202    /// the sequence counter, and captures a wall-clock timestamp.
203    pub fn extract(sample: &gstreamer::Sample, seq: &Arc<AtomicU64>) -> Result<Self, MediaError> {
204        use std::sync::atomic::Ordering;
205
206        let caps = sample.caps().ok_or_else(|| MediaError::DecodeFailed {
207            detail: "sample has no caps".into(),
208        })?;
209
210        let video_info =
211            gstreamer_video::VideoInfo::from_caps(caps).map_err(|e| MediaError::DecodeFailed {
212                detail: format!("failed to parse VideoInfo from caps: {e}"),
213            })?;
214
215        let buffer = sample
216            .buffer_owned()
217            .ok_or_else(|| MediaError::DecodeFailed {
218                detail: "sample has no buffer".into(),
219            })?;
220
221        let pts_ns = buffer.pts().map(|pts| pts.nseconds()).unwrap_or(0);
222
223        Ok(Self {
224            width: video_info.width(),
225            height: video_info.height(),
226            stride: video_info.stride()[0] as u32,
227            ts: nv_core::timestamp::MonotonicTs::from_nanos(pts_ns),
228            wall_ts: nv_core::timestamp::WallTs::now(),
229            frame_seq: seq.fetch_add(1, Ordering::Relaxed),
230            buffer,
231        })
232    }
233
234    /// Build a device-resident [`FrameEnvelope`] from this sample info.
235    ///
236    /// Assembles the `TypedMetadata` (including optional PTZ telemetry)
237    /// and delegates to [`FrameEnvelope::new_device`].
238    pub fn into_device_envelope(
239        self,
240        feed_id: nv_core::id::FeedId,
241        pixel_format: PixelFormat,
242        handle: Arc<dyn std::any::Any + Send + Sync>,
243        materialize: Option<nv_frame::HostMaterializeFn>,
244        ptz: Option<crate::bridge::PtzTelemetry>,
245    ) -> FrameEnvelope {
246        let mut metadata = nv_core::TypedMetadata::new();
247        if let Some(telemetry) = ptz {
248            metadata.insert(telemetry);
249        }
250
251        FrameEnvelope::new_device(
252            feed_id,
253            self.frame_seq,
254            self.ts,
255            self.wall_ts,
256            self.width,
257            self.height,
258            pixel_format,
259            self.stride,
260            handle,
261            materialize,
262            metadata,
263        )
264    }
265}