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}