Skip to main content

videotoolbox/
error.rs

1//! Errors produced by `VideoToolbox` APIs.
2
3use core::fmt;
4
5use crate::ffi::OSStatus;
6
7/// Top-level error returned by all fallible APIs in this crate.
8///
9/// Wraps Apple's `OSStatus` and tags the call site so the user can tell
10/// "the encoder failed to create" apart from "the encoder rejected this frame".
11#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum VTError {
14    /// `VTCompressionSessionCreate` returned non-zero.
15    SessionCreateFailed(OSStatus),
16    /// `VTSessionSetProperty` returned non-zero. The String identifies which property.
17    SetPropertyFailed { key: String, status: OSStatus },
18    /// `VTCompressionSessionPrepareToEncodeFrames` returned non-zero.
19    PrepareFailed(OSStatus),
20    /// `VTCompressionSessionEncodeFrame` returned non-zero.
21    EncodeFailed(OSStatus),
22    /// `VTCompressionSessionCompleteFrames` returned non-zero.
23    CompleteFailed(OSStatus),
24    /// `CVPixelBufferCreateWithIOSurface` returned non-zero or NULL.
25    PixelBufferCreateFailed(i32),
26    /// The user-supplied async callback reported a non-zero status when
27    /// encoding a frame.
28    EncoderCallback(OSStatus),
29    /// A specific `VideoToolbox` / `CoreVideo` / `CoreMedia` API returned non-zero.
30    ApiFailed { api: &'static str, status: OSStatus },
31    /// A one-shot operation received a buffer containing an unsupported number of samples.
32    UnexpectedSampleCount {
33        operation: &'static str,
34        expected: usize,
35        actual: i64,
36    },
37    /// A bounded bridge operation did not finish before its deadline.
38    TimedOut { operation: &'static str },
39    /// An invalid argument was supplied (e.g. zero width).
40    InvalidArgument(String),
41    Unsupported {
42        api: &'static str,
43        minimum: &'static str,
44    },
45    #[cfg(feature = "frame_processor")]
46    CommandBuffer(apple_metal::CommandBufferError),
47}
48
49impl VTError {
50    /// Underlying `OSStatus` if the error originated from a VT call.
51    #[must_use]
52    pub const fn status(&self) -> Option<OSStatus> {
53        match self {
54            Self::SessionCreateFailed(s)
55            | Self::SetPropertyFailed { status: s, .. }
56            | Self::PrepareFailed(s)
57            | Self::EncodeFailed(s)
58            | Self::CompleteFailed(s)
59            | Self::EncoderCallback(s)
60            | Self::ApiFailed { status: s, .. } => Some(*s),
61            Self::PixelBufferCreateFailed(_)
62            | Self::UnexpectedSampleCount { .. }
63            | Self::TimedOut { .. }
64            | Self::InvalidArgument(_)
65            | Self::Unsupported { .. } => None,
66            #[cfg(feature = "frame_processor")]
67            Self::CommandBuffer(_) => None,
68        }
69    }
70}
71
72impl fmt::Display for VTError {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            Self::SessionCreateFailed(s) => write!(f, "VTCompressionSessionCreate failed: {s}"),
76            Self::SetPropertyFailed { key, status } => {
77                write!(f, "VTSessionSetProperty({key:?}) failed: {status}")
78            }
79            Self::PrepareFailed(s) => {
80                write!(f, "VTCompressionSessionPrepareToEncodeFrames failed: {s}")
81            }
82            Self::EncodeFailed(s) => write!(f, "VTCompressionSessionEncodeFrame failed: {s}"),
83            Self::CompleteFailed(s) => write!(f, "VTCompressionSessionCompleteFrames failed: {s}"),
84            Self::PixelBufferCreateFailed(s) => {
85                write!(f, "CVPixelBufferCreateWithIOSurface failed: {s}")
86            }
87            Self::EncoderCallback(s) => write!(f, "encoder callback reported status {s}"),
88            Self::ApiFailed { api, status } => write!(f, "{api} failed: {status}"),
89            Self::UnexpectedSampleCount {
90                operation,
91                expected,
92                actual,
93            } => write!(
94                f,
95                "{operation} requires exactly {expected} sample, but the buffer contains {actual}"
96            ),
97            Self::TimedOut { operation } => write!(f, "{operation} timed out"),
98            Self::InvalidArgument(m) => write!(f, "invalid argument: {m}"),
99            Self::Unsupported { api, minimum } => {
100                write!(f, "{api} requires macOS {minimum} or later")
101            }
102            #[cfg(feature = "frame_processor")]
103            Self::CommandBuffer(error) => write!(f, "command buffer: {error}"),
104        }
105    }
106}
107
108impl std::error::Error for VTError {
109    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
110        match self {
111            #[cfg(feature = "frame_processor")]
112            Self::CommandBuffer(error) => Some(error),
113            _ => None,
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::VTError;
121
122    #[test]
123    fn status_returns_underlying_osstatus_when_available() {
124        assert_eq!(VTError::SessionCreateFailed(-12903).status(), Some(-12903));
125        assert_eq!(
126            VTError::SetPropertyFailed {
127                key: "RealTime".to_owned(),
128                status: -50,
129            }
130            .status(),
131            Some(-50)
132        );
133        assert_eq!(
134            VTError::ApiFailed {
135                api: "VTSessionCopyProperty",
136                status: -7,
137            }
138            .status(),
139            Some(-7)
140        );
141    }
142
143    #[test]
144    fn status_returns_none_for_non_osstatus_variants() {
145        assert_eq!(VTError::PixelBufferCreateFailed(-666).status(), None);
146        assert_eq!(
147            VTError::InvalidArgument("width must be positive".to_owned()).status(),
148            None
149        );
150        assert_eq!(
151            VTError::UnexpectedSampleCount {
152                operation: "decode_frame_async",
153                expected: 1,
154                actual: 2,
155            }
156            .status(),
157            None
158        );
159        assert_eq!(
160            VTError::TimedOut {
161                operation: "RAW frame processing",
162            }
163            .status(),
164            None
165        );
166        assert_eq!(
167            VTError::Unsupported {
168                api: "VTDecompressionSessionDecodeFrameWithOptions",
169                minimum: "15.0",
170            }
171            .status(),
172            None
173        );
174    }
175
176    #[test]
177    fn display_formats_property_and_api_failures() {
178        assert_eq!(
179            VTError::SetPropertyFailed {
180                key: "ProfileLevel".to_owned(),
181                status: -12902,
182            }
183            .to_string(),
184            "VTSessionSetProperty(\"ProfileLevel\") failed: -12902"
185        );
186        assert_eq!(
187            VTError::ApiFailed {
188                api: "VTSessionCopyProperty",
189                status: -7,
190            }
191            .to_string(),
192            "VTSessionCopyProperty failed: -7"
193        );
194    }
195
196    #[test]
197    fn display_formats_argument_and_pixel_buffer_failures() {
198        assert_eq!(
199            VTError::InvalidArgument("zero width".to_owned()).to_string(),
200            "invalid argument: zero width"
201        );
202        assert_eq!(
203            VTError::PixelBufferCreateFailed(-666).to_string(),
204            "CVPixelBufferCreateWithIOSurface failed: -666"
205        );
206        assert_eq!(
207            VTError::EncoderCallback(-8971).to_string(),
208            "encoder callback reported status -8971"
209        );
210        assert_eq!(
211            VTError::UnexpectedSampleCount {
212                operation: "DecompressionSession::decode_frame_async",
213                expected: 1,
214                actual: 2,
215            }
216            .to_string(),
217            "DecompressionSession::decode_frame_async requires exactly 1 sample, but the buffer contains 2"
218        );
219        assert_eq!(
220            VTError::TimedOut {
221                operation: "RAW frame processing",
222            }
223            .to_string(),
224            "RAW frame processing timed out"
225        );
226        assert_eq!(
227            VTError::Unsupported {
228                api: "VTDecompressionSessionDecodeFrameWithOptions",
229                minimum: "15.0",
230            }
231            .to_string(),
232            "VTDecompressionSessionDecodeFrameWithOptions requires macOS 15.0 or later"
233        );
234    }
235}