// Oak Video Editor - Non-Linear Video Editor // Copyright (C) 2026 Oak Team // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! End-to-end export: a sequence carrying one clip renders through //! [`oak_task::export::ExportTask`] into a real H.264/AAC MP4 — the file //! exists, probes to the sequence geometry, and decodes back to the //! clip's content. This is the "export is a fake" report's regression //! guard: the whole chain (graph montage -> render tickets -> encoder) //! must produce a playable file. use std::sync::{Arc, Mutex}; use oak_core::{PixelFormat, Rational, TimeRange}; use oak_node::block::ClipBlockBehavior; use oak_node::footage::FootageBehavior; use oak_node::id::NodeId; use oak_node::node::NodeCore; use oak_node::project::Project; use oak_node::sequence::SequenceBehavior; use oak_node::track::{TrackBehavior, TrackListBehavior}; use oak_render::manager::{RenderBackendChoice, RenderManager}; /// Unique temp path per test (the process id disambiguates parallel test /// binaries; the tag separates files inside one binary). fn clip_path(tag: &str) -> std::path::PathBuf { std::env::temp_dir().join(format!("oaktask_export_{tag}_{}.mp4", std::process::id())) } /// One sequence + one video track list + one track + one clip of `media` /// covering `[0, 1s)`. fn build_project(media: &str) -> (Arc>, NodeId) { // The export converts through the pipeline working space on the way // out; pin the legacy sRGB pass-through so the decoded-back pixels // keep their display-referred values for the assertions below. oak_core::color::set_pipeline_color_settings( oak_core::colormath::WorkingColorSpace::SrgbLegacy, oak_core::colormath::OutputColorSpec::default(), ); let project = Project::new(); let seq; { let mut p = project.lock().unwrap(); let (score, sbehavior) = SequenceBehavior::create(); seq = p.graph.add_node(score, sbehavior); let (tcore, tbehavior) = TrackListBehavior::create(); let tl = p.graph.add_node(tcore, tbehavior); let (tcore, tbehavior) = TrackBehavior::create(); let track = p.graph.add_node(tcore, tbehavior); let mut footage = FootageBehavior::new(media); footage.probe().expect("probe the generated clip"); let footage = p.graph.add_node(NodeCore::new(), Box::new(footage)); let (ccore, cbehavior) = oak_node::block::clip_create(); let clip = p.graph.add_node(ccore, cbehavior); p.graph .connect(footage, clip, oak_node::block::clip_input::TEXTURE_INPUT, -1) .expect("connect footage to clip"); p.graph .get_mut(clip) .unwrap() .behavior .as_any_mut() .unwrap() .downcast_mut::() .expect("clip block") .core .range = TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)); p.graph .get_mut(track) .unwrap() .behavior .as_any_mut() .unwrap() .downcast_mut::() .expect("video track") .append_block(clip); p.graph .get_mut(tl) .unwrap() .behavior .as_any_mut() .unwrap() .downcast_mut::() .expect("video track list") .tracks .push(track); p.graph .get_mut(seq) .unwrap() .behavior .as_any_mut() .unwrap() .downcast_mut::() .expect("sequence") .track_lists .push(tl); } (project, seq) } /// The full export chain: a 64x64 one-second sequence exports to MP4; /// the result probes to 64x64 and decodes back to the clip's solid green. #[test] fn export_writes_a_real_playable_mp4() { let media = clip_path("src"); oak_codec::testmedia::write_test_clip_solid(&media, 64, 64, 10, 10, [0.0, 1.0, 0.0, 1.0]) .expect("green clip generation"); let (project, seq) = build_project(&media.to_string_lossy()); let out = clip_path("out"); let _ = std::fs::remove_file(&out); // The export's render loop runs on the process-wide manager arena; // the inline backend keeps the test in-process. if RenderManager::global().is_none() { RenderManager::init_with_backend(RenderBackendChoice::Threads) .expect("render manager init"); } let encoding = oak_task::export::EncodingParams { filename: out.to_string_lossy().into_owned(), format: oak_codec::exportformat::Format::MPEG4Video as i32, video_enabled: true, video_codec: oak_codec::exportcodec::Codec::H264 as i32, video_width: 64, video_height: 64, video_time_base_num: 1, video_time_base_den: 30, video_pixel_format: 0, audio_enabled: true, audio_codec: oak_codec::exportcodec::Codec::AAC as i32, audio_sample_rate: 48000, audio_channel_layout: 0x3, subtitles_enabled: false, export_length_num: 1, export_length_den: 1, has_custom_range: false, custom_range_in_num: 0, custom_range_in_den: 1, custom_range_out_num: 0, custom_range_out_den: 1, video_bit_rate: 0, audio_bit_rate: 0, color_override_enabled: false, color_primaries: 0, color_trc: 0, color_space: 0, }; let inner = oak_task::export::ExportTask::new((project.clone(), seq), encoding); let mut driver = oak_task::task::Task::new("Exporting...", None); driver.set_behavior(Box::new(inner)); if let Err(e) = driver.start() { panic!( "export failed: {e:?} / {}", driver.error().unwrap_or("unknown error") ); } // The file exists and is a real container (video + audio for a second // is far past a header-only file). let bytes = std::fs::metadata(&out) .expect("the export wrote the file") .len(); assert!( bytes > 1024, "a playable mp4 with video+audio is more than 1K, got {bytes}" ); // It probes to the sequence geometry. let mut probe = FootageBehavior::new(&out.to_string_lossy()); probe.probe().expect("the export probes as media"); let params = probe .video_params(0) .expect("the export has a video stream"); assert_eq!( (params.width, params.height), (64, 64), "the export's video stream is the sequence geometry" ); // And a decoded frame is the clip's solid green (H.264 YCbCr keeps // the hue; loose thresholds). let tex = oak_render::eval::render_footage_frame( &out.to_string_lossy(), 0, Rational::new(0, 1), (64, 64), PixelFormat::F32, ) .expect("decode the exported frame"); let oak_core::texture::Texture::Cpu(frame) = &tex else { panic!("decode produced a GPU texture"); }; let off = 32 * frame.linesize_bytes() as usize + 32 * 16; let r = f32::from_le_bytes(frame.data[off..off + 4].try_into().unwrap()); let g = f32::from_le_bytes(frame.data[off + 4..off + 8].try_into().unwrap()); assert!( g > 0.4 && r < 0.3, "the exported frame is the clip's green (r={r} g={g})" ); let _ = std::fs::remove_file(&media); let _ = std::fs::remove_file(&out); }