media_pp/core/contract.rs
1//! What a port promises about the buffers passing through it.
2//!
3//! This is a deliberately conservative link check, not caps negotiation.
4//! It answers one question — "can these two elements possibly be wired
5//! together?" — from information every element already knows when it is
6//! constructed, and it answers "I don't know" whenever it isn't sure.
7//!
8//! What it does *not* do, on purpose: it never picks a codec, never
9//! inserts a converter, never renegotiates mid-stream, and never
10//! reallocates a pool. Pixel format, resolution, stride, color space, and
11//! device identity stay where they already are — validated against the
12//! real buffer when it arrives, by the element that is about to use it.
13//! A contract only rules out wiring that could never have worked at all,
14//! such as feeding encoded packets into an encoder that only accepts
15//! decoded video, wiring a container's audio stream into a video decoder,
16//! or handing a D3D11 texture to a CUDA filter.
17//!
18//! Declaring a contract is opt-in: both sides default to
19//! [`InputContract::Unknown`] / [`OutputContract::Unknown`], which always
20//! links, so an element outside this crate keeps working untouched. This
21//! crate's own elements do declare one, with the deliberate exceptions of
22//! [`AppSource`](crate::elements::AppSource) — only the application knows
23//! what it will push — and a demuxer pad for a medium not modelled here.
24
25use std::fmt;
26
27use ffmpeg_next as ffmpeg;
28
29/// Which [`MediaBuffer`](crate::buffer::MediaBuffer) payloads a port deals
30/// in, split by medium as well as by encoding.
31///
32/// The medium is part of the kind because
33/// [`MediaBuffer::Packet`](crate::buffer::MediaBuffer::Packet) alone does
34/// not carry it: a demuxer's audio and video pads emit the same variant,
35/// so without this split, wiring a container's audio stream into a video
36/// decoder is a link the check cannot see. Every element that deals in
37/// packets does know its own medium when it is constructed — from the
38/// stream parameters it was opened with, or from being an audio encoder
39/// rather than a video one — so the distinction costs nothing to state.
40///
41/// [`MediaBuffer::Eos`](crate::buffer::MediaBuffer::Eos) is deliberately
42/// absent: every sink must accept EOS, so it is never part of what a
43/// contract can rule out.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum MediaKind {
46 /// Encoded video, as [`MediaBuffer::Packet`](crate::buffer::MediaBuffer::Packet).
47 VideoPacket,
48 /// Encoded audio, as [`MediaBuffer::Packet`](crate::buffer::MediaBuffer::Packet).
49 AudioPacket,
50 /// Decoded [`MediaBuffer::Video`](crate::buffer::MediaBuffer::Video).
51 VideoFrame,
52 /// Decoded [`MediaBuffer::Audio`](crate::buffer::MediaBuffer::Audio).
53 AudioFrame,
54}
55
56impl MediaKind {
57 /// The encoded kind a stream of `medium` carries, or `None` for a
58 /// medium none of this crate's elements handle (subtitles, data). A
59 /// caller with no kind to state declares
60 /// [`OutputContract::Unknown`]/[`InputContract::Unknown`] and leaves
61 /// that pad to the runtime check, rather than guessing.
62 pub fn packet_for(medium: ffmpeg::media::Type) -> Option<Self> {
63 match medium {
64 ffmpeg::media::Type::Video => Some(MediaKind::VideoPacket),
65 ffmpeg::media::Type::Audio => Some(MediaKind::AudioPacket),
66 _ => None,
67 }
68 }
69
70 const fn bit(self) -> u8 {
71 match self {
72 MediaKind::VideoPacket => 1 << 0,
73 MediaKind::AudioPacket => 1 << 1,
74 MediaKind::VideoFrame => 1 << 2,
75 MediaKind::AudioFrame => 1 << 3,
76 }
77 }
78
79 const ALL: [MediaKind; 4] = [
80 MediaKind::VideoPacket,
81 MediaKind::AudioPacket,
82 MediaKind::VideoFrame,
83 MediaKind::AudioFrame,
84 ];
85}
86
87impl fmt::Display for MediaKind {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 let name = match self {
90 MediaKind::VideoPacket => "VideoPacket",
91 MediaKind::AudioPacket => "AudioPacket",
92 MediaKind::VideoFrame => "VideoFrame",
93 MediaKind::AudioFrame => "AudioFrame",
94 };
95 f.write_str(name)
96 }
97}
98
99/// A set of [`MediaKind`]s, as a port rarely deals in exactly one.
100///
101/// A set rather than a single kind because the two sides mean different
102/// things: a producer's set is everything it *may* emit, a consumer's is
103/// everything it *can* accept, and compatibility is the former being a
104/// subset of the latter. A demuxer feeding a muxer may emit either
105/// encoded kind, while a video decoder accepts only one of them — a
106/// distinction a single kind could not express.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct MediaKindSet(u8);
109
110impl MediaKindSet {
111 /// A set holding exactly `kind`.
112 pub const fn of(kind: MediaKind) -> Self {
113 Self(kind.bit())
114 }
115
116 /// A set holding every kind in `kinds`. Duplicates are harmless.
117 pub const fn from_slice(kinds: &[MediaKind]) -> Self {
118 let mut bits = 0;
119 let mut index = 0;
120 while index < kinds.len() {
121 bits |= kinds[index].bit();
122 index += 1;
123 }
124 Self(bits)
125 }
126
127 /// Both encoded kinds — what a muxer, a packet counter, or any other
128 /// element that interleaves or forwards encoded media deals in.
129 pub const PACKETS: Self = Self::from_slice(&[MediaKind::VideoPacket, MediaKind::AudioPacket]);
130
131 /// Both decoded kinds — what an element that handles frames without
132 /// caring which medium they are deals in.
133 pub const FRAMES: Self = Self::from_slice(&[MediaKind::VideoFrame, MediaKind::AudioFrame]);
134
135 /// Returns whether `kind` is in this set.
136 pub const fn contains(self, kind: MediaKind) -> bool {
137 self.0 & kind.bit() != 0
138 }
139
140 /// Returns whether every kind in this set is also in `other` — the
141 /// producer-into-consumer direction the link check asks about.
142 pub const fn is_subset_of(self, other: Self) -> bool {
143 self.0 & !other.0 == 0
144 }
145}
146
147impl fmt::Display for MediaKindSet {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 let mut first = true;
150 for kind in MediaKind::ALL {
151 if !self.contains(kind) {
152 continue;
153 }
154 if !first {
155 f.write_str("|")?;
156 }
157 write!(f, "{kind}")?;
158 first = false;
159 }
160 if first {
161 f.write_str("nothing")
162 } else {
163 Ok(())
164 }
165 }
166}
167
168/// Where a decoded frame's pixels actually live.
169///
170/// [`MediaBuffer::Video`](crate::buffer::MediaBuffer::Video) is one variant
171/// covering system memory, CUDA device memory, and D3D11/D3D12 textures
172/// alike, so the buffer type alone cannot tell a CPU scaler that it was
173/// handed a GPU texture. This is the part of the contract that catches
174/// that — it says which backend owns the memory, and nothing about the
175/// format, size, or specific device within that backend.
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum MemoryDomain {
178 /// Host memory: an ordinary FFmpeg frame with CPU-readable planes.
179 System,
180 /// CUDA device memory bound to a CUDA context.
181 Cuda,
182 /// A D3D11 texture owned by an `ID3D11Device`.
183 D3d11,
184 /// A D3D12 resource owned by an `ID3D12Device`.
185 D3d12,
186}
187
188impl MemoryDomain {
189 const fn bit(self) -> u8 {
190 match self {
191 MemoryDomain::System => 1 << 0,
192 MemoryDomain::Cuda => 1 << 1,
193 MemoryDomain::D3d11 => 1 << 2,
194 MemoryDomain::D3d12 => 1 << 3,
195 }
196 }
197
198 const ALL: [MemoryDomain; 4] = [
199 MemoryDomain::System,
200 MemoryDomain::Cuda,
201 MemoryDomain::D3d11,
202 MemoryDomain::D3d12,
203 ];
204}
205
206impl fmt::Display for MemoryDomain {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 let name = match self {
209 MemoryDomain::System => "System",
210 MemoryDomain::Cuda => "CUDA",
211 MemoryDomain::D3d11 => "D3D11",
212 MemoryDomain::D3d12 => "D3D12",
213 };
214 f.write_str(name)
215 }
216}
217
218/// A set of [`MemoryDomain`]s — everywhere a port's frames may live.
219///
220/// A producer's set is what it may emit and a consumer's is what it can
221/// take, so compatibility is the former being a subset of the latter,
222/// exactly as for [`MediaKindSet`]. An element that genuinely does not
223/// care — one that never reads the pixels — declares [`Self::ALL`], which
224/// is a claim rather than an omission: there is no "unstated" domain to
225/// forget, because [`PortContract::Frames`] has nowhere to leave it out.
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub struct MemoryDomainSet(u8);
228
229impl MemoryDomainSet {
230 /// Every backend — for an element that passes frames through without
231 /// reading them.
232 pub const ALL: Self = Self::from_slice(&MemoryDomain::ALL);
233
234 /// A set holding exactly `domain`.
235 pub const fn of(domain: MemoryDomain) -> Self {
236 Self(domain.bit())
237 }
238
239 /// A set holding every domain in `domains`. Duplicates are harmless.
240 pub const fn from_slice(domains: &[MemoryDomain]) -> Self {
241 let mut bits = 0;
242 let mut index = 0;
243 while index < domains.len() {
244 bits |= domains[index].bit();
245 index += 1;
246 }
247 Self(bits)
248 }
249
250 /// Returns whether `domain` is in this set.
251 pub const fn contains(self, domain: MemoryDomain) -> bool {
252 self.0 & domain.bit() != 0
253 }
254
255 /// Returns whether every domain in this set is also in `other` — the
256 /// producer-into-consumer direction the link check asks about.
257 pub const fn is_subset_of(self, other: Self) -> bool {
258 self.0 & !other.0 == 0
259 }
260}
261
262impl fmt::Display for MemoryDomainSet {
263 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264 if *self == Self::ALL {
265 return f.write_str("any memory");
266 }
267 let mut first = true;
268 for domain in MemoryDomain::ALL {
269 if !self.contains(domain) {
270 continue;
271 }
272 if !first {
273 f.write_str("|")?;
274 }
275 write!(f, "{domain}")?;
276 first = false;
277 }
278 if first {
279 f.write_str("nothing")
280 } else {
281 Ok(())
282 }
283 }
284}
285
286/// What one port deals in.
287///
288/// Split by encoding rather than carrying an optional domain, because the
289/// two halves ask different questions. Encoded media is always host
290/// memory, so [`Self::Packets`] has nowhere to put a domain and nowhere to
291/// forget one. Decoded frames always live somewhere specific, so
292/// [`Self::Frames`] always states it — an element that genuinely takes any
293/// backend says [`MemoryDomainSet::ALL`], which reads as the deliberate
294/// claim it is rather than as an omission.
295///
296/// The two never link to each other. That falls out of the shape, and it
297/// matches [`MediaKind`]: no packet kind is a frame kind.
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub enum PortContract {
300 /// Encoded media — [`MediaKind::VideoPacket`]/[`MediaKind::AudioPacket`].
301 Packets(MediaKindSet),
302 /// Decoded frames — [`MediaKind::VideoFrame`]/[`MediaKind::AudioFrame`]
303 /// — together with the backends their memory may live in.
304 Frames(MediaKindSet, MemoryDomainSet),
305}
306
307impl PortContract {
308 /// One encoded kind.
309 pub const fn packet(kind: MediaKind) -> Self {
310 Self::Packets(MediaKindSet::of(kind))
311 }
312
313 /// One decoded kind in one backend's memory.
314 pub const fn frame(kind: MediaKind, memory: MemoryDomain) -> Self {
315 Self::Frames(MediaKindSet::of(kind), MemoryDomainSet::of(memory))
316 }
317
318 /// One decoded kind, wherever it lives — for an element that forwards
319 /// or counts frames without reading them.
320 pub const fn any_frame(kind: MediaKind) -> Self {
321 Self::Frames(MediaKindSet::of(kind), MemoryDomainSet::ALL)
322 }
323
324 /// Returns whether a producer emitting `produced` can feed a consumer
325 /// accepting `self`.
326 ///
327 /// Every kind the producer may emit has to be accepted, and so does
328 /// every domain its frames may live in. Encoded media and decoded
329 /// frames never satisfy each other.
330 pub fn accepts(&self, produced: &PortContract) -> bool {
331 match (self, produced) {
332 (PortContract::Packets(accepted), PortContract::Packets(produced)) => {
333 produced.is_subset_of(*accepted)
334 }
335 (
336 PortContract::Frames(accepted, accepted_memory),
337 PortContract::Frames(produced, produced_memory),
338 ) => produced.is_subset_of(*accepted) && produced_memory.is_subset_of(*accepted_memory),
339 _ => false,
340 }
341 }
342}
343
344impl fmt::Display for PortContract {
345 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346 match self {
347 PortContract::Packets(kinds) => write!(f, "{kinds}"),
348 PortContract::Frames(kinds, memory) => write!(f, "{kinds} ({memory})"),
349 }
350 }
351}
352
353/// What a [`Sink`](crate::element::Sink) can be fed.
354///
355/// [`Any`](Self::Any) and [`Unknown`](Self::Unknown) both link to
356/// anything, but they mean opposite things and differ in what happens
357/// *downstream* of the element — see [`OutputContract::Passthrough`].
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum InputContract {
360 /// This element accepts exactly this and nothing else.
361 Fixed(PortContract),
362
363 /// A guarantee that every [`MediaKind`] is handled. A
364 /// [`Queue`](crate::queue::Queue) forwards whatever it is given; an
365 /// [`AppSink`](crate::elements::AppSink) hands every buffer to its
366 /// closure. Note the scope: this promises the *element* passes each
367 /// kind along, not that the application's own closure will succeed
368 /// with it. A closure that only understands packets still returns its
369 /// own error, which is outside what a link check can or should know.
370 Any,
371
372 /// No claim. Links to anything, and stops the check from continuing
373 /// past this element, because nothing here knows what comes out.
374 Unknown,
375}
376
377impl fmt::Display for InputContract {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 match self {
380 InputContract::Fixed(contract) => write!(f, "{contract}"),
381 InputContract::Any => f.write_str("anything"),
382 InputContract::Unknown => f.write_str("unknown"),
383 }
384 }
385}
386
387/// What a [`SrcPad`](crate::pad::SrcPad) emits.
388///
389/// Declared per pad rather than per element because
390/// [`Tee`](crate::elements::Tee) and
391/// [`FileDemuxer`](crate::elements::FileDemuxer) own several, and nothing
392/// requires them to agree.
393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394pub enum OutputContract {
395 /// This pad emits exactly this and nothing else.
396 Fixed(PortContract),
397
398 /// Whatever arrived on the input leaves here unchanged — a
399 /// [`Queue`](crate::queue::Queue), a [`Tee`](crate::elements::Tee), a
400 /// [`Pacer`](crate::elements::Pacer). This is what keeps a check alive
401 /// across the middle of a pipeline: the upstream contract is carried
402 /// through, so a decoder's output still meets an encoder's input two
403 /// queues later.
404 Passthrough,
405
406 /// No claim. The check goes dark from here on.
407 Unknown,
408}
409
410impl fmt::Display for OutputContract {
411 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412 match self {
413 OutputContract::Fixed(contract) => write!(f, "{contract}"),
414 OutputContract::Passthrough => f.write_str("whatever it receives"),
415 OutputContract::Unknown => f.write_str("unknown"),
416 }
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn a_producers_kinds_must_all_be_accepted() {
426 let video_only = PortContract::any_frame(MediaKind::VideoFrame);
427 let both = PortContract::Frames(MediaKindSet::FRAMES, MemoryDomainSet::ALL);
428
429 assert!(video_only.accepts(&video_only));
430 assert!(both.accepts(&video_only));
431 // The audio half of `both` has nowhere to go in a video-only sink.
432 assert!(!video_only.accepts(&both));
433 }
434
435 /// The split the medium exists for: both are `MediaBuffer::Packet`, so
436 /// nothing else separates a container's audio stream from its video one.
437 #[test]
438 fn encoded_audio_and_encoded_video_are_different_kinds() {
439 let video = PortContract::packet(MediaKind::VideoPacket);
440 let audio = PortContract::packet(MediaKind::AudioPacket);
441
442 assert!(!video.accepts(&audio));
443 assert!(!audio.accepts(&video));
444 // A muxer takes either, and a demuxer pad of either kind fits it.
445 let muxer = PortContract::Packets(MediaKindSet::PACKETS);
446 assert!(muxer.accepts(&video));
447 assert!(muxer.accepts(&audio));
448 }
449
450 /// Encoded and decoded ports never satisfy each other, and the shape
451 /// is what says so — there is no domain to compare across them.
452 #[test]
453 fn packets_and_frames_never_link() {
454 let packets = PortContract::Packets(MediaKindSet::PACKETS);
455 let frames = PortContract::Frames(MediaKindSet::FRAMES, MemoryDomainSet::ALL);
456
457 assert!(!packets.accepts(&frames));
458 assert!(!frames.accepts(&packets));
459 }
460
461 #[test]
462 fn a_medium_maps_to_its_encoded_kind_or_to_nothing() {
463 assert_eq!(
464 MediaKind::packet_for(ffmpeg::media::Type::Video),
465 Some(MediaKind::VideoPacket)
466 );
467 assert_eq!(
468 MediaKind::packet_for(ffmpeg::media::Type::Audio),
469 Some(MediaKind::AudioPacket)
470 );
471 // Subtitles and data streams are not modelled, and a caller with
472 // no kind to state leaves that pad Unknown rather than guessing.
473 assert_eq!(MediaKind::packet_for(ffmpeg::media::Type::Subtitle), None);
474 }
475
476 /// Domains are a set on both sides now, so "takes any backend" is a
477 /// claim an element makes rather than a field it left empty.
478 #[test]
479 fn every_domain_a_producer_may_emit_must_be_accepted() {
480 let system = PortContract::frame(MediaKind::VideoFrame, MemoryDomain::System);
481 let d3d11 = PortContract::frame(MediaKind::VideoFrame, MemoryDomain::D3d11);
482 let anywhere = PortContract::any_frame(MediaKind::VideoFrame);
483
484 assert!(system.accepts(&system));
485 assert!(!system.accepts(&d3d11));
486 // A pass-through element takes either; neither takes everything.
487 assert!(anywhere.accepts(&d3d11));
488 assert!(anywhere.accepts(&system));
489 assert!(!d3d11.accepts(&anywhere));
490 }
491
492 #[test]
493 fn kinds_render_for_diagnostics() {
494 assert_eq!(
495 PortContract::packet(MediaKind::VideoPacket).to_string(),
496 "VideoPacket"
497 );
498 assert_eq!(
499 PortContract::frame(MediaKind::VideoFrame, MemoryDomain::D3d11).to_string(),
500 "VideoFrame (D3D11)"
501 );
502 assert_eq!(
503 PortContract::any_frame(MediaKind::VideoFrame).to_string(),
504 "VideoFrame (any memory)"
505 );
506 assert_eq!(MediaKindSet::PACKETS.to_string(), "VideoPacket|AudioPacket");
507 }
508}