lumen-engine-ffmpeg 0.2.1

FFmpeg integration for media decode, encode, muxing, and GPU interop in Lumen.
Documentation
use std::{
    fs,
    path::PathBuf,
    time::{Duration, SystemTime, UNIX_EPOCH},
};

use crate::gpu::{GpuBackend, GpuVideoInput};
use crate::video::{EncodeMode, VideoCodec};

use super::{
    muxed::MuxedEncoder,
    telemetry::{GpuEncodeTelemetry, GpuUploadDescriptor},
    video::VideoEncoderConfig,
};

fn temp_path(name: &str, extension: &str) -> PathBuf {
    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock")
        .as_nanos();
    std::env::temp_dir().join(format!("lumen_ffmpeg_{name}_{unique}.{extension}"))
}

#[cfg(feature = "vulkan")]
#[test]
fn telemetry_tracks_upload_and_encode_events() {
    use ash::vk::Handle;

    use crate::gpu::VulkanVideoFrame;

    let frame = VulkanVideoFrame::new(
        ash::vk::Image::from_raw(1),
        ash::vk::ImageView::from_raw(2),
        ash::vk::DeviceMemory::from_raw(3),
        ash::vk::Format::R8G8B8A8_UNORM,
        ash::vk::Extent3D {
            width: 1280,
            height: 720,
            depth: 1,
        },
    );
    let input = GpuVideoInput::Vulkan(&frame);
    let descriptor = GpuUploadDescriptor::from_frame(&input);
    let mut telemetry = GpuEncodeTelemetry::default();

    telemetry.record_upload_started(&descriptor);
    telemetry.record_upload_finished(&descriptor, Duration::from_micros(12));
    telemetry.record_encode_started(&descriptor);
    telemetry.record_encode_failed(&descriptor, Duration::from_micros(34), "not yet");

    assert_eq!(telemetry.upload_attempts, 1);
    assert_eq!(telemetry.upload_successes, 1);
    assert_eq!(telemetry.encode_attempts, 1);
    assert_eq!(telemetry.encode_failures, 1);
    assert_eq!(telemetry.vulkan_frames, 1);
    assert_eq!(telemetry.last_error.as_deref(), Some("not yet"));
    assert_eq!(telemetry.recent_events.len(), 4);
}

#[cfg(feature = "cuda")]
#[test]
fn cuda_telemetry_tracks_upload_attempts() {
    use crate::gpu::CudaVideoFrame;

    let frame = CudaVideoFrame::from_device_ptr(0xABCD, 1920, 1080, 8192, Some(12));
    let input = GpuVideoInput::Cuda(&frame);
    let descriptor = GpuUploadDescriptor::from_frame(&input);
    let mut telemetry = GpuEncodeTelemetry::default();

    telemetry.record_upload_started(&descriptor);
    telemetry.record_upload_failed(&descriptor, Duration::from_micros(25), "not imported");

    assert_eq!(telemetry.upload_attempts, 1);
    assert_eq!(telemetry.upload_failures, 1);
    assert_eq!(telemetry.cuda_frames, 1);
    assert_eq!(telemetry.last_error.as_deref(), Some("not imported"));
    assert_eq!(telemetry.recent_events.len(), 2);
}

#[cfg(feature = "vulkan")]
#[test]
fn cpu_upload_encoder_rejects_gpu_input() {
    use ash::vk::Handle;

    use crate::gpu::VulkanVideoFrame;

    let path = temp_path("gpu_failure", "mp4");
    let Ok(mut encoder) = MuxedEncoder::create(
        path.to_string_lossy().to_string(),
        VideoEncoderConfig::cpu_rgba(16, 16, 30, VideoCodec::H264),
    ) else {
        eprintln!("H.264 encoder unavailable; skipping GPU input mismatch test");
        return;
    };
    let frame = VulkanVideoFrame::new(
        ash::vk::Image::from_raw(1),
        ash::vk::ImageView::from_raw(2),
        ash::vk::DeviceMemory::from_raw(3),
        ash::vk::Format::R8G8B8A8_UNORM,
        ash::vk::Extent3D {
            width: 16,
            height: 16,
            depth: 1,
        },
    );
    let input = GpuVideoInput::Vulkan(&frame);

    let error = encoder
        .write_gpu_frame(&input)
        .expect_err("CPU encoder should reject GPU input");
    assert_eq!(error.backend, Some(GpuBackend::Vulkan));
    assert_eq!(encoder.gpu_telemetry().upload_attempts, 0);
    assert_eq!(encoder.gpu_telemetry().upload_failures, 0);

    let _ = fs::remove_file(path);
}

#[cfg(feature = "vulkan")]
#[test]
fn unavailable_hardware_texture_encoder_errors_at_create() {
    let path = temp_path("gpu_create_failure", "mp4");
    let mut config = VideoEncoderConfig::cpu_rgba(16, 16, 30, VideoCodec::H264);
    config.mode = EncodeMode::GpuTexture(GpuBackend::Vulkan);

    let error = match MuxedEncoder::create(path.to_string_lossy().to_string(), config) {
        Ok(_) => panic!("Vulkan texture encode should fail at create"),
        Err(error) => error,
    };
    assert_eq!(error.operation, "VideoEncoder::create");
    assert_eq!(error.backend, Some(GpuBackend::Vulkan));

    let _ = fs::remove_file(path);
}