videotoolbox 0.21.0

Safe Rust bindings for Apple's VideoToolbox framework — hardware H.264/HEVC/ProRes encode and decode on macOS
Documentation
//! Errors produced by `VideoToolbox` APIs.

use core::fmt;

use crate::ffi::OSStatus;

/// Top-level error returned by all fallible APIs in this crate.
///
/// Wraps Apple's `OSStatus` and tags the call site so the user can tell
/// "the encoder failed to create" apart from "the encoder rejected this frame".
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum VTError {
    /// `VTCompressionSessionCreate` returned non-zero.
    SessionCreateFailed(OSStatus),
    /// `VTSessionSetProperty` returned non-zero. The String identifies which property.
    SetPropertyFailed { key: String, status: OSStatus },
    /// `VTCompressionSessionPrepareToEncodeFrames` returned non-zero.
    PrepareFailed(OSStatus),
    /// `VTCompressionSessionEncodeFrame` returned non-zero.
    EncodeFailed(OSStatus),
    /// `VTCompressionSessionCompleteFrames` returned non-zero.
    CompleteFailed(OSStatus),
    /// `CVPixelBufferCreateWithIOSurface` returned non-zero or NULL.
    PixelBufferCreateFailed(i32),
    /// The user-supplied async callback reported a non-zero status when
    /// encoding a frame.
    EncoderCallback(OSStatus),
    /// A specific `VideoToolbox` / `CoreVideo` / `CoreMedia` API returned non-zero.
    ApiFailed { api: &'static str, status: OSStatus },
    /// A one-shot operation received a buffer containing an unsupported number of samples.
    UnexpectedSampleCount {
        operation: &'static str,
        expected: usize,
        actual: i64,
    },
    /// A bounded bridge operation did not finish before its deadline.
    TimedOut { operation: &'static str },
    /// An invalid argument was supplied (e.g. zero width).
    InvalidArgument(String),
    Unsupported {
        api: &'static str,
        minimum: &'static str,
    },
    #[cfg(feature = "frame_processor")]
    CommandBuffer(apple_metal::CommandBufferError),
}

impl VTError {
    /// Underlying `OSStatus` if the error originated from a VT call.
    #[must_use]
    pub const fn status(&self) -> Option<OSStatus> {
        match self {
            Self::SessionCreateFailed(s)
            | Self::SetPropertyFailed { status: s, .. }
            | Self::PrepareFailed(s)
            | Self::EncodeFailed(s)
            | Self::CompleteFailed(s)
            | Self::EncoderCallback(s)
            | Self::ApiFailed { status: s, .. } => Some(*s),
            Self::PixelBufferCreateFailed(_)
            | Self::UnexpectedSampleCount { .. }
            | Self::TimedOut { .. }
            | Self::InvalidArgument(_)
            | Self::Unsupported { .. } => None,
            #[cfg(feature = "frame_processor")]
            Self::CommandBuffer(_) => None,
        }
    }
}

impl fmt::Display for VTError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::SessionCreateFailed(s) => write!(f, "VTCompressionSessionCreate failed: {s}"),
            Self::SetPropertyFailed { key, status } => {
                write!(f, "VTSessionSetProperty({key:?}) failed: {status}")
            }
            Self::PrepareFailed(s) => {
                write!(f, "VTCompressionSessionPrepareToEncodeFrames failed: {s}")
            }
            Self::EncodeFailed(s) => write!(f, "VTCompressionSessionEncodeFrame failed: {s}"),
            Self::CompleteFailed(s) => write!(f, "VTCompressionSessionCompleteFrames failed: {s}"),
            Self::PixelBufferCreateFailed(s) => {
                write!(f, "CVPixelBufferCreateWithIOSurface failed: {s}")
            }
            Self::EncoderCallback(s) => write!(f, "encoder callback reported status {s}"),
            Self::ApiFailed { api, status } => write!(f, "{api} failed: {status}"),
            Self::UnexpectedSampleCount {
                operation,
                expected,
                actual,
            } => write!(
                f,
                "{operation} requires exactly {expected} sample, but the buffer contains {actual}"
            ),
            Self::TimedOut { operation } => write!(f, "{operation} timed out"),
            Self::InvalidArgument(m) => write!(f, "invalid argument: {m}"),
            Self::Unsupported { api, minimum } => {
                write!(f, "{api} requires macOS {minimum} or later")
            }
            #[cfg(feature = "frame_processor")]
            Self::CommandBuffer(error) => write!(f, "command buffer: {error}"),
        }
    }
}

impl std::error::Error for VTError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            #[cfg(feature = "frame_processor")]
            Self::CommandBuffer(error) => Some(error),
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::VTError;

    #[test]
    fn status_returns_underlying_osstatus_when_available() {
        assert_eq!(VTError::SessionCreateFailed(-12903).status(), Some(-12903));
        assert_eq!(
            VTError::SetPropertyFailed {
                key: "RealTime".to_owned(),
                status: -50,
            }
            .status(),
            Some(-50)
        );
        assert_eq!(
            VTError::ApiFailed {
                api: "VTSessionCopyProperty",
                status: -7,
            }
            .status(),
            Some(-7)
        );
    }

    #[test]
    fn status_returns_none_for_non_osstatus_variants() {
        assert_eq!(VTError::PixelBufferCreateFailed(-666).status(), None);
        assert_eq!(
            VTError::InvalidArgument("width must be positive".to_owned()).status(),
            None
        );
        assert_eq!(
            VTError::UnexpectedSampleCount {
                operation: "decode_frame_async",
                expected: 1,
                actual: 2,
            }
            .status(),
            None
        );
        assert_eq!(
            VTError::TimedOut {
                operation: "RAW frame processing",
            }
            .status(),
            None
        );
        assert_eq!(
            VTError::Unsupported {
                api: "VTDecompressionSessionDecodeFrameWithOptions",
                minimum: "15.0",
            }
            .status(),
            None
        );
    }

    #[test]
    fn display_formats_property_and_api_failures() {
        assert_eq!(
            VTError::SetPropertyFailed {
                key: "ProfileLevel".to_owned(),
                status: -12902,
            }
            .to_string(),
            "VTSessionSetProperty(\"ProfileLevel\") failed: -12902"
        );
        assert_eq!(
            VTError::ApiFailed {
                api: "VTSessionCopyProperty",
                status: -7,
            }
            .to_string(),
            "VTSessionCopyProperty failed: -7"
        );
    }

    #[test]
    fn display_formats_argument_and_pixel_buffer_failures() {
        assert_eq!(
            VTError::InvalidArgument("zero width".to_owned()).to_string(),
            "invalid argument: zero width"
        );
        assert_eq!(
            VTError::PixelBufferCreateFailed(-666).to_string(),
            "CVPixelBufferCreateWithIOSurface failed: -666"
        );
        assert_eq!(
            VTError::EncoderCallback(-8971).to_string(),
            "encoder callback reported status -8971"
        );
        assert_eq!(
            VTError::UnexpectedSampleCount {
                operation: "DecompressionSession::decode_frame_async",
                expected: 1,
                actual: 2,
            }
            .to_string(),
            "DecompressionSession::decode_frame_async requires exactly 1 sample, but the buffer contains 2"
        );
        assert_eq!(
            VTError::TimedOut {
                operation: "RAW frame processing",
            }
            .to_string(),
            "RAW frame processing timed out"
        );
        assert_eq!(
            VTError::Unsupported {
                api: "VTDecompressionSessionDecodeFrameWithOptions",
                minimum: "15.0",
            }
            .to_string(),
            "VTDecompressionSessionDecodeFrameWithOptions requires macOS 15.0 or later"
        );
    }
}