media-pp 0.1.7

A small, GStreamer-flavored media pipeline library built on FFmpeg.
Documentation
//! Shared fixtures for this crate's own tests.

/// Path to a real video file for tests that need one, from
/// `MEDIA_PP_TEST_VIDEO`. Returns `None` — after printing why — when the
/// variable is unset or names something that is not a readable file, the same
/// way a hardware test's `try_device()` skips on a machine without the device.
///
/// No media is checked into this repository, so there is no default to fall
/// back to:
///
/// ```text
/// MEDIA_PP_TEST_VIDEO=/path/to/video.mp4 cargo test -p media-pp
/// ```
///
/// Any container `FileDemuxer` can open works, as long as it holds a video
/// stream and runs for at least a few seconds — the seek tests pace playback
/// and then reposition, so a clip shorter than that finishes before they get
/// to it. Nothing depends on a particular codec, resolution, or keyframe
/// spacing; a test that would need one must assert the contract instead (see
/// `pipeline::tests::seek_reports_where_it_actually_landed_when_target_is_not_a_keyframe`).
///
/// Tests using this must still assert real behavior when it does return a
/// path — skipping is for the machine that has no fixture, not a way to make
/// a failing assertion optional.
pub(crate) fn try_test_video() -> Option<String> {
    let Ok(path) = std::env::var("MEDIA_PP_TEST_VIDEO") else {
        eprintln!(
            "skipping: set MEDIA_PP_TEST_VIDEO to a video file to run this test \
             (no media is checked into this repository)"
        );
        return None;
    };
    if !std::path::Path::new(&path).is_file() {
        eprintln!("skipping: MEDIA_PP_TEST_VIDEO=`{path}` is not a readable file");
        return None;
    }
    eprintln!("using test video: {path}");
    Some(path)
}

/// A CUDA device for a hardware test, together with the lock that keeps
/// CUDA tests from overlapping. `None` — after printing why — on a machine
/// without a usable device, the same way [`try_test_video`] skips without a
/// fixture.
///
/// The two are returned together because they are not separable in practice.
/// Creating or destroying a `CudaDevice` retains/releases the *process-wide*
/// CUDA primary context (see [`crate::elements::CudaDevice`]'s own docs on
/// why it uses that context), and doing so on one thread while another
/// thread has NVDEC or NVENC work in flight segfaults inside `libnvcuvid` —
/// on a thread the driver itself owns, so nothing in this crate can catch or
/// recover it.
///
/// Running the whole suite hid that, because cheap tests were interleaved
/// between the CUDA ones often enough to keep them from overlapping; a run
/// filtered down to CUDA tests alone (`cargo test --features cuda cuda_`)
/// crashed reliably. Bind the guard for the body of the test:
///
/// ```ignore
/// let Some((device, _cuda_lock)) = try_cuda_device() else {
///     return;
/// };
/// ```
///
/// A test needing a *second* device — to check that a frame from a foreign
/// context is rejected — calls `CudaDevice::new()` directly rather than this
/// again: the lock is already held, and it does not nest.
#[cfg(feature = "cuda")]
pub(crate) fn try_cuda_device() -> Option<(
    crate::elements::CudaDevice,
    std::sync::MutexGuard<'static, ()>,
)> {
    static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
    // A test that panics while holding this must not turn every later CUDA
    // test into a poison error instead of its own real result.
    let guard = LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
    match crate::elements::CudaDevice::new() {
        Ok(device) => Some((device, guard)),
        Err(error) => {
            eprintln!("skipping: no usable CUDA device on this machine ({error})");
            None
        }
    }
}