Skip to main content

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}