1use bytes::Bytes;
6use oximedia_audio::{AudioBuffer, AudioFrame, ChannelLayout};
7use oximedia_codec::VideoFrame;
8use oximedia_core::{PixelFormat, Rational, SampleFormat, Timestamp};
9use std::collections::{HashMap, VecDeque};
10use std::path::PathBuf;
11use std::sync::Arc;
12#[cfg(not(target_arch = "wasm32"))]
13use tokio::sync::mpsc;
14
15use crate::clip::Clip;
16use crate::error::EditResult;
17use crate::frame_prefetch::{PrefetchConfig, PrefetchEngine};
18use crate::incremental_render::DirtyRegion;
19use crate::parallel_render::{
20 render_tracks_parallel, ClipWithSource, TrackKind, TrackRenderInput, TrackRenderOutput,
21};
22use crate::render_source::RenderSource;
23use crate::timeline::{Timeline, TimelineConfig, TrackType};
24use crate::transition::Transition;
25
26pub struct TimelineRenderer {
28 timeline: Arc<Timeline>,
30 config: RenderConfig,
32 cache: FrameCache,
34 raw_frame_cache: RawFrameCache,
36 source_cache: HashMap<PathBuf, Arc<RenderSource>>,
39 dirty_regions: Vec<DirtyRegion>,
42 prefetch: PrefetchEngine,
45 use_parallel: bool,
48}
49
50impl TimelineRenderer {
51 #[must_use]
53 pub fn new(timeline: Arc<Timeline>, config: RenderConfig) -> Self {
54 let cache_size = config.cache_size;
55 let max_pos = timeline.duration.max(0) as i64;
56 let prefetch_config = PrefetchConfig::for_playback(30.0, 1.0);
57 Self {
58 timeline,
59 config,
60 cache: FrameCache::new(cache_size),
61 raw_frame_cache: RawFrameCache::new(RAW_FRAME_CACHE_CAPACITY),
62 source_cache: HashMap::new(),
63 dirty_regions: Vec::new(),
64 prefetch: PrefetchEngine::new(prefetch_config, max_pos),
65 use_parallel: false,
66 }
67 }
68
69 pub fn mark_dirty(&mut self, start_frame: u64, end_frame: u64) {
75 self.dirty_regions
76 .push(DirtyRegion::new(start_frame, end_frame));
77 self.coalesce_dirty();
78 }
79
80 pub fn clear_dirty(&mut self) {
82 self.dirty_regions.clear();
83 }
84
85 pub fn force_full_redraw(&mut self) {
87 let total = self.timeline.duration.unsigned_abs();
88 self.dirty_regions = vec![DirtyRegion::new(0, total.max(1))];
89 }
90
91 #[must_use]
96 pub fn is_position_dirty(&self, position: i64) -> bool {
97 if self.dirty_regions.is_empty() {
98 return true;
99 }
100 let frame = position.unsigned_abs();
101 self.dirty_regions.iter().any(|r| r.contains(frame))
102 }
103
104 #[must_use]
106 pub fn prefetch_playhead(&self) -> i64 {
107 self.prefetch.playhead()
108 }
109
110 pub fn set_use_parallel(&mut self, enabled: bool) {
112 self.use_parallel = enabled;
113 }
114
115 #[must_use]
117 pub fn use_parallel(&self) -> bool {
118 self.use_parallel
119 }
120
121 fn coalesce_dirty(&mut self) {
124 if self.dirty_regions.len() <= 1 {
125 return;
126 }
127 self.dirty_regions.sort_by_key(|r| r.start_frame);
128 let mut merged: Vec<DirtyRegion> = Vec::with_capacity(self.dirty_regions.len());
129 for region in &self.dirty_regions {
130 if let Some(last) = merged.last_mut() {
131 if last.end_frame >= region.start_frame {
132 last.end_frame = last.end_frame.max(region.end_frame);
133 continue;
134 }
135 }
136 merged.push(*region);
137 }
138 self.dirty_regions = merged;
139 }
140
141 pub async fn render_frame_at(&mut self, position: i64) -> EditResult<RenderFrame> {
143 if let Some(frame) = self.cache.get(position) {
145 return Ok(frame);
146 }
147
148 let clips_at_pos: Vec<(usize, Clip)> = self
151 .timeline
152 .get_clips_at(position)
153 .into_iter()
154 .map(|(ti, c)| (ti, c.clone()))
155 .collect();
156 let clips_refs: Vec<(usize, &Clip)> = clips_at_pos.iter().map(|(ti, c)| (*ti, c)).collect();
157
158 let video_frame = if self.config.render_video {
160 self.render_video_at(position, &clips_refs).await?
161 } else {
162 None
163 };
164
165 let audio_frame = if self.config.render_audio {
167 self.render_audio_at(position, &clips_refs).await?
168 } else {
169 None
170 };
171
172 let frame = RenderFrame {
173 position,
174 timestamp: Timestamp::new(position, self.timeline.timebase),
175 video: video_frame,
176 audio: audio_frame,
177 };
178
179 self.cache.put(position, frame.clone());
181
182 let _ = self.prefetch.update(position);
184
185 Ok(frame)
186 }
187
188 async fn render_video_at(
190 &mut self,
191 position: i64,
192 clips: &[(usize, &Clip)],
193 ) -> EditResult<Option<VideoFrame>> {
194 let video_clips: Vec<(usize, Clip)> = clips
195 .iter()
196 .filter(|(_, clip)| clip.is_video())
197 .map(|(ti, c)| (*ti, (*c).clone()))
198 .collect();
199
200 if video_clips.is_empty() {
201 return Ok(None);
202 }
203
204 if !self.is_position_dirty(position) {
206 return Ok(None);
207 }
208
209 let w = self.config.width;
210 let h = self.config.height;
211
212 if self.use_parallel {
214 return self.render_video_at_parallel(position, w, h);
215 }
216
217 let active_transitions: Vec<(usize, Transition)> = video_clips
220 .iter()
221 .flat_map(|(track_idx, _)| {
222 self.timeline
223 .transitions
224 .get_active_at(*track_idx, position)
225 .into_iter()
226 .map(|t| (*track_idx, t.clone()))
227 })
228 .collect();
229
230 let mut in_transition_pair: HashMap<u64, (VideoFrame, VideoFrame, Transition, f64)> =
234 HashMap::new();
235 let mut transitioned_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
236
237 for (_, transition) in &active_transitions {
238 let clip_a_id = transition.clip_a;
239 let clip_b_id = transition.clip_b;
240
241 let clip_a = video_clips
242 .iter()
243 .find(|(_, c)| c.id == clip_a_id)
244 .map(|(_, c)| c.clone());
245 let clip_b = video_clips
246 .iter()
247 .find(|(_, c)| c.id == clip_b_id)
248 .map(|(_, c)| c.clone());
249
250 if let (Some(ca), Some(cb)) = (clip_a, clip_b) {
251 let pos_a = ca.timeline_to_source(position);
252 let pos_b = cb.timeline_to_source(position);
253 let frame_a = self.get_source_frame(&ca, pos_a)?;
254 let frame_b = self.get_source_frame(&cb, pos_b)?;
255 let progress = transition.progress_at(position);
256 transitioned_ids.insert(clip_a_id);
257 transitioned_ids.insert(clip_b_id);
258 in_transition_pair.entry(clip_a_id).or_insert((
260 frame_a,
261 frame_b,
262 transition.clone(),
263 progress,
264 ));
265 }
266 }
267
268 use oximedia_graphics::hdr_composite::HdrCompositor;
270
271 let mut compositor = HdrCompositor::new(w, h, 1000.0);
272
273 for (_, clip) in video_clips.iter().rev() {
275 if clip.muted {
276 continue;
277 }
278
279 let source_pos = clip.timeline_to_source(position);
280
281 if transitioned_ids.contains(&clip.id) {
282 if let Some((fa, fb, trans, progress)) = in_transition_pair.remove(&clip.id) {
284 let blended = TransitionRenderer::blend_video(&fa, &fb, &trans, progress);
285 let layer = video_frame_to_hdr_layer(&blended, clip.opacity);
286 compositor.add_layer(layer);
287 }
288 continue;
290 }
291
292 let source_frame = self.get_source_frame(clip, source_pos)?;
293 let layer = video_frame_to_hdr_layer(&source_frame, clip.opacity);
294 compositor.add_layer(layer);
295 }
296
297 let rgba_f32 = compositor.composite();
299 let mut output = VideoFrame::new(self.config.pixel_format, w, h);
300 output.allocate();
301 output.timestamp = Timestamp::new(position, self.timeline.timebase);
302
303 fill_output_frame_from_rgba_f32(&mut output, &rgba_f32, w, h);
305
306 Ok(Some(output))
307 }
308
309 fn render_video_at_parallel(
315 &mut self,
316 position: i64,
317 w: u32,
318 h: u32,
319 ) -> EditResult<Option<VideoFrame>> {
320 let config_clone = self.config.clone();
322 let timeline_clone = self.timeline.clone();
323
324 let inputs: Vec<TrackRenderInput> = timeline_clone
325 .tracks
326 .iter()
327 .filter(|t| !t.muted && matches!(t.track_type, TrackType::Video))
328 .map(|track| {
329 let active_clips: Vec<ClipWithSource> = track
330 .clips
331 .iter()
332 .filter(|c| c.contains(position) && !c.muted)
333 .map(|c| {
334 let source = self.resolve_source(c);
335 ClipWithSource {
336 clip: c.clone(),
337 source,
338 }
339 })
340 .collect();
341 TrackRenderInput::video(track.index, active_clips, position, w, h)
342 })
343 .collect();
344
345 if inputs.is_empty() {
346 return Ok(None);
347 }
348
349 let outputs: Vec<TrackRenderOutput> = render_tracks_parallel(&inputs);
351
352 use oximedia_graphics::hdr_composite::HdrCompositor;
356
357 let mut compositor = HdrCompositor::new(w, h, 1000.0);
358 for out in outputs.iter().rev() {
359 if out.kind != TrackKind::Video || out.video_rgba8.is_empty() {
360 continue;
361 }
362 use oximedia_graphics::hdr_composite::HdrLayer;
365 let pixel_count = (w as usize) * (h as usize);
366 let mut layer = HdrLayer::new(w, h);
367 layer.opacity = 1.0;
368 for i in 0..pixel_count {
369 let base = i * 4;
370 if base + 3 < out.video_rgba8.len() {
371 layer.pixels[base] = out.video_rgba8[base] as f32 / 255.0;
372 layer.pixels[base + 1] = out.video_rgba8[base + 1] as f32 / 255.0;
373 layer.pixels[base + 2] = out.video_rgba8[base + 2] as f32 / 255.0;
374 layer.pixels[base + 3] = out.video_rgba8[base + 3] as f32 / 255.0;
375 }
376 }
377 compositor.add_layer(layer);
378 }
379
380 let rgba_f32 = compositor.composite();
381 let mut output = VideoFrame::new(config_clone.pixel_format, w, h);
382 output.allocate();
383 output.timestamp = Timestamp::new(position, self.timeline.timebase);
384 fill_output_frame_from_rgba_f32(&mut output, &rgba_f32, w, h);
385
386 Ok(Some(output))
387 }
388
389 async fn render_audio_at(
391 &mut self,
392 position: i64,
393 clips: &[(usize, &Clip)],
394 ) -> EditResult<Option<AudioFrame>> {
395 let audio_clips: Vec<(usize, Clip)> = clips
396 .iter()
397 .filter(|(_, clip)| clip.is_audio())
398 .map(|(ti, c)| (*ti, (*c).clone()))
399 .collect();
400
401 if audio_clips.is_empty() {
402 return Ok(None);
403 }
404
405 let ch_count = self.config.channels.count();
406 let num_samples: usize = 1024;
408 let mut mix_buf = vec![0.0_f32; num_samples * ch_count];
409
410 use crate::transition::TransitionType;
413
414 let crossfade_transitions: Vec<(usize, Transition)> = audio_clips
415 .iter()
416 .flat_map(|(track_idx, _)| {
417 self.timeline
418 .transitions
419 .get_active_at(*track_idx, position)
420 .into_iter()
421 .filter(|t| matches!(t.transition_type, TransitionType::CrossFade))
422 .map(|t| (*track_idx, t.clone()))
423 })
424 .collect();
425
426 let mut crossfade_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
428 let mut crossfade_pairs: Vec<(AudioFrame, AudioFrame, Transition, f64)> = Vec::new();
429
430 for (_, transition) in &crossfade_transitions {
431 let ca_id = transition.clip_a;
432 let cb_id = transition.clip_b;
433
434 let ca = audio_clips
435 .iter()
436 .find(|(_, c)| c.id == ca_id)
437 .map(|(_, c)| (*c).clone());
438 let cb = audio_clips
439 .iter()
440 .find(|(_, c)| c.id == cb_id)
441 .map(|(_, c)| (*c).clone());
442 if let (Some(ca), Some(cb)) = (ca, cb) {
443 let fa = self.get_source_audio_frame(&ca, ca.timeline_to_source(position))?;
444 let fb = self.get_source_audio_frame(&cb, cb.timeline_to_source(position))?;
445 let progress = transition.progress_at(position);
446 crossfade_ids.insert(ca_id);
447 crossfade_ids.insert(cb_id);
448 crossfade_pairs.push((fa, fb, transition.clone(), progress));
449 }
450 }
451
452 for (fa, fb, trans, progress) in &crossfade_pairs {
454 let blended = TransitionRenderer::mix_audio(fa, fb, trans, *progress);
455 accumulate_audio_frame_into(&mut mix_buf, &blended, 1.0, ch_count, num_samples);
456 }
457
458 for (_, clip) in audio_clips.iter() {
460 if clip.muted || crossfade_ids.contains(&clip.id) {
461 continue;
462 }
463 let source_pos = clip.timeline_to_source(position);
464 let src_frame = self.get_source_audio_frame(clip, source_pos)?;
465 let gain = clip.opacity; accumulate_audio_frame_into(&mut mix_buf, &src_frame, gain, ch_count, num_samples);
467 }
468
469 for s in &mut mix_buf {
471 *s = s.clamp(-1.0, 1.0);
472 }
473
474 let bytes: Vec<u8> = mix_buf.iter().flat_map(|s| s.to_ne_bytes()).collect();
476
477 let mut output = AudioFrame {
478 format: self.config.sample_format,
479 sample_rate: self.config.sample_rate,
480 channels: self.config.channels.clone(),
481 samples: oximedia_audio::AudioBuffer::Interleaved(Bytes::from(bytes)),
482 timestamp: Timestamp::new(position, self.timeline.timebase),
483 };
484 output.timestamp = Timestamp::new(position, self.timeline.timebase);
485
486 Ok(Some(output))
487 }
488
489 fn resolve_source(&mut self, clip: &Clip) -> Arc<RenderSource> {
491 match &clip.source {
492 None => Arc::new(RenderSource::TestPattern),
493 Some(path) => {
494 if let Some(cached) = self.source_cache.get(path) {
495 return cached.clone();
496 }
497 let resolved = RenderSource::from_path(path)
498 .unwrap_or_else(|_| Arc::new(RenderSource::TestPattern));
499 self.source_cache.insert(path.clone(), resolved.clone());
500 resolved
501 }
502 }
503 }
504
505 fn get_source_frame(&mut self, clip: &Clip, source_pts: i64) -> EditResult<VideoFrame> {
508 let w = self.config.width;
509 let h = self.config.height;
510
511 let key = (clip.id.wrapping_mul(0x9e3779b9)).wrapping_add(source_pts.unsigned_abs())
515 ^ (source_pts.signum() as u64);
516
517 let source = self.resolve_source(clip);
518
519 let source_clone = source.clone();
523 let rgba8 = self
524 .raw_frame_cache
525 .get_or_render(key, || source_clone.sample_video(source_pts, w, h))
526 .to_vec();
527
528 let mut frame = VideoFrame::new(self.config.pixel_format, w, h);
529 frame.allocate();
530 fill_output_frame_from_rgba8(&mut frame, &rgba8, w, h);
531 Ok(frame)
532 }
533
534 fn get_source_audio_frame(&mut self, clip: &Clip, source_pts: i64) -> EditResult<AudioFrame> {
536 let ch_count = self.config.channels.count() as u16;
537 let sample_rate = self.config.sample_rate;
538 let num_samples: usize = 1024;
539
540 let source = self.resolve_source(clip);
541 let samples = source.sample_audio(source_pts, num_samples, ch_count, sample_rate);
542
543 let bytes: Vec<u8> = samples.iter().flat_map(|s| s.to_ne_bytes()).collect();
544
545 Ok(AudioFrame {
546 format: self.config.sample_format,
547 sample_rate,
548 channels: self.config.channels.clone(),
549 samples: oximedia_audio::AudioBuffer::Interleaved(Bytes::from(bytes)),
550 timestamp: Timestamp::new(source_pts, self.timeline.timebase),
551 })
552 }
553
554 pub fn start_background_render(&mut self) -> BackgroundRenderer {
556 BackgroundRenderer::new(self.timeline.clone(), self.config.clone())
557 }
558
559 pub fn clear_cache(&mut self) {
561 self.cache.clear();
562 }
563}
564
565fn video_frame_to_hdr_layer(
573 frame: &VideoFrame,
574 opacity: f32,
575) -> oximedia_graphics::hdr_composite::HdrLayer {
576 use oximedia_graphics::hdr_composite::HdrLayer;
577
578 let w = frame.width;
579 let h = frame.height;
580 let pixel_count = (w as usize) * (h as usize);
581 let mut layer = HdrLayer::new(w, h);
582 layer.opacity = opacity.clamp(0.0, 1.0);
583
584 if let Some(plane) = frame.planes.first() {
587 for i in 0..pixel_count {
588 let idx = i * 4;
589 let luma = if i < plane.data.len() {
590 plane.data[i] as f32 / 255.0
591 } else {
592 0.0_f32
593 };
594 layer.pixels[idx] = luma;
595 layer.pixels[idx + 1] = luma;
596 layer.pixels[idx + 2] = luma;
597 layer.pixels[idx + 3] = 1.0;
598 }
599 }
600
601 layer
602}
603
604fn fill_output_frame_from_rgba_f32(frame: &mut VideoFrame, rgba: &[f32], w: u32, h: u32) {
608 let pixel_count = (w as usize) * (h as usize);
609 if let Some(plane) = frame.planes.first_mut() {
610 let out_len = plane.data.len().min(pixel_count);
611 for i in 0..out_len {
612 let base = i * 4;
613 if base + 2 < rgba.len() {
614 let luma =
616 (0.2126 * rgba[base] + 0.7152 * rgba[base + 1] + 0.0722 * rgba[base + 2])
617 .clamp(0.0, 1.0);
618 #[allow(clippy::cast_possible_truncation)]
619 #[allow(clippy::cast_sign_loss)]
620 let y = (luma * 255.0).round() as u8;
621 plane.data[i] = y;
622 }
623 }
624 }
625}
626
627fn fill_output_frame_from_rgba8(frame: &mut VideoFrame, rgba8: &[u8], w: u32, h: u32) {
629 let pixel_count = (w as usize) * (h as usize);
630 if let Some(plane) = frame.planes.first_mut() {
631 let out_len = plane.data.len().min(pixel_count);
632 for i in 0..out_len {
633 let base = i * 4;
634 if base + 2 < rgba8.len() {
635 let r = rgba8[base] as u32;
637 let g = rgba8[base + 1] as u32;
638 let b = rgba8[base + 2] as u32;
639 let y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
640 plane.data[i] = y.min(255) as u8;
641 }
642 }
643 }
644}
645
646fn accumulate_audio_frame_into(
649 mix_buf: &mut [f32],
650 frame: &AudioFrame,
651 gain: f32,
652 ch_count: usize,
653 num_samples: usize,
654) {
655 use oximedia_mixer::simd_audio::mix_and_gain_simd;
656
657 let expected = num_samples * ch_count;
658 let src_samples: Vec<f32> = match &frame.samples {
659 oximedia_audio::AudioBuffer::Interleaved(bytes) => bytes
660 .chunks_exact(4)
661 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
662 .take(expected)
663 .collect(),
664 oximedia_audio::AudioBuffer::Planar(planes) => {
665 let frames_per_plane = if planes.is_empty() {
667 0
668 } else {
669 (planes[0].len() / 4).min(num_samples)
670 };
671 let mut interleaved = vec![0.0_f32; frames_per_plane * ch_count];
672 for (c, plane) in planes.iter().enumerate().take(ch_count) {
673 for f in 0..frames_per_plane {
674 let base = f * 4;
675 if base + 3 < plane.len() {
676 interleaved[f * ch_count + c] = f32::from_ne_bytes([
677 plane[base],
678 plane[base + 1],
679 plane[base + 2],
680 plane[base + 3],
681 ]);
682 }
683 }
684 }
685 interleaved
686 }
687 };
688
689 if src_samples.is_empty() {
690 return;
691 }
692
693 let dst_len = mix_buf.len().min(expected);
694 let src_len = src_samples.len().min(dst_len);
695 mix_and_gain_simd(&mut mix_buf[..dst_len], &src_samples[..src_len], gain);
696}
697
698#[derive(Clone, Debug)]
700pub struct RenderFrame {
701 pub position: i64,
703 pub timestamp: Timestamp,
705 pub video: Option<VideoFrame>,
707 pub audio: Option<AudioFrame>,
709}
710
711impl RenderFrame {
712 #[must_use]
714 pub fn has_video(&self) -> bool {
715 self.video.is_some()
716 }
717
718 #[must_use]
720 pub fn has_audio(&self) -> bool {
721 self.audio.is_some()
722 }
723}
724
725#[derive(Clone, Debug)]
727pub struct RenderConfig {
728 pub render_video: bool,
730 pub render_audio: bool,
732 pub width: u32,
734 pub height: u32,
736 pub pixel_format: PixelFormat,
738 pub sample_rate: u32,
740 pub sample_format: SampleFormat,
742 pub channels: ChannelLayout,
744 pub cache_size: usize,
746 pub num_threads: usize,
748 pub quality: RenderQuality,
750}
751
752impl Default for RenderConfig {
753 fn default() -> Self {
754 Self {
755 render_video: true,
756 render_audio: true,
757 width: 1920,
758 height: 1080,
759 pixel_format: PixelFormat::Yuv420p,
760 sample_rate: 48000,
761 sample_format: SampleFormat::F32,
762 channels: ChannelLayout::Stereo,
763 cache_size: 30,
764 num_threads: 4,
765 quality: RenderQuality::High,
766 }
767 }
768}
769
770impl RenderConfig {
771 #[must_use]
773 pub fn from_timeline_config(config: &TimelineConfig) -> Self {
774 Self {
775 width: config.width,
776 height: config.height,
777 sample_rate: config.sample_rate,
778 channels: ChannelLayout::from_count(config.channels as usize),
779 ..Default::default()
780 }
781 }
782}
783
784#[derive(Clone, Copy, Debug, PartialEq, Eq)]
786pub enum RenderQuality {
787 Draft,
789 Preview,
791 High,
793 Maximum,
795}
796
797impl RenderQuality {
798 #[must_use]
800 pub fn factor(&self) -> f32 {
801 match self {
802 Self::Draft => 0.25,
803 Self::Preview => 0.5,
804 Self::High => 0.75,
805 Self::Maximum => 1.0,
806 }
807 }
808}
809
810#[derive(Debug)]
812struct FrameCache {
813 frames: VecDeque<(i64, RenderFrame)>,
815 capacity: usize,
817}
818
819impl FrameCache {
820 fn new(capacity: usize) -> Self {
822 Self {
823 frames: VecDeque::with_capacity(capacity),
824 capacity,
825 }
826 }
827
828 fn get(&self, position: i64) -> Option<RenderFrame> {
830 self.frames
831 .iter()
832 .find(|(pos, _)| *pos == position)
833 .map(|(_, frame)| frame.clone())
834 }
835
836 fn put(&mut self, position: i64, frame: RenderFrame) {
838 if self.frames.len() >= self.capacity {
840 self.frames.pop_front();
841 }
842 self.frames.push_back((position, frame));
843 }
844
845 fn clear(&mut self) {
847 self.frames.clear();
848 }
849}
850
851#[cfg(not(target_arch = "wasm32"))]
853pub struct BackgroundRenderer {
854 timeline: Arc<Timeline>,
856 config: RenderConfig,
858 handle: Option<tokio::task::JoinHandle<()>>,
860}
861
862#[cfg(not(target_arch = "wasm32"))]
863impl BackgroundRenderer {
864 #[must_use]
866 pub fn new(timeline: Arc<Timeline>, config: RenderConfig) -> Self {
867 Self {
868 timeline,
869 config,
870 handle: None,
871 }
872 }
873
874 pub fn start(&mut self, start: i64, end: i64) -> mpsc::Receiver<RenderFrame> {
876 let (tx, rx) = mpsc::channel(100);
877 let timeline = self.timeline.clone();
878 let config = self.config.clone();
879
880 let handle = tokio::spawn(async move {
881 let mut renderer = TimelineRenderer::new(timeline, config);
882
883 for position in start..end {
884 match renderer.render_frame_at(position).await {
885 Ok(frame) => {
886 if tx.send(frame).await.is_err() {
887 break;
888 }
889 }
890 Err(_) => break,
891 }
892 }
893 });
894
895 self.handle = Some(handle);
896 rx
897 }
898
899 pub async fn stop(&mut self) {
901 if let Some(handle) = self.handle.take() {
902 handle.abort();
903 let _ = handle.await;
904 }
905 }
906
907 #[must_use]
909 pub fn is_complete(&self) -> bool {
910 self.handle
911 .as_ref()
912 .map_or(true, tokio::task::JoinHandle::is_finished)
913 }
914}
915
916pub struct PreviewRenderer {
918 renderer: TimelineRenderer,
920 frame_rate: Rational,
922 position: i64,
924 playing: bool,
926}
927
928impl PreviewRenderer {
929 #[must_use]
931 pub fn new(timeline: Arc<Timeline>, config: RenderConfig) -> Self {
932 let frame_rate = timeline.frame_rate;
933 Self {
934 renderer: TimelineRenderer::new(timeline, config),
935 frame_rate,
936 position: 0,
937 playing: false,
938 }
939 }
940
941 pub fn play(&mut self) {
943 self.playing = true;
944 }
945
946 pub fn pause(&mut self) {
948 self.playing = false;
949 }
950
951 pub fn stop(&mut self) {
953 self.playing = false;
954 self.position = 0;
955 }
956
957 pub async fn next_frame(&mut self) -> EditResult<Option<RenderFrame>> {
959 if !self.playing {
960 return Ok(None);
961 }
962
963 let frame = self.renderer.render_frame_at(self.position).await?;
964
965 #[allow(clippy::cast_possible_truncation)]
967 #[allow(clippy::cast_precision_loss)]
968 let frame_duration = (1000.0 / self.frame_rate.to_f64()) as i64;
969 self.position += frame_duration;
970
971 if self.position >= self.renderer.timeline.duration {
973 self.stop();
974 }
975
976 Ok(Some(frame))
977 }
978
979 pub fn seek(&mut self, position: i64) {
981 self.position = position.clamp(0, self.renderer.timeline.duration);
982 }
983
984 #[must_use]
986 pub fn position(&self) -> i64 {
987 self.position
988 }
989
990 #[must_use]
992 pub fn is_playing(&self) -> bool {
993 self.playing
994 }
995}
996
997pub struct ExportRenderer {
999 renderer: TimelineRenderer,
1001 settings: ExportSettings,
1003}
1004
1005impl ExportRenderer {
1006 #[must_use]
1008 pub fn new(timeline: Arc<Timeline>, settings: ExportSettings) -> Self {
1009 let config = RenderConfig {
1010 render_video: settings.video_enabled,
1011 render_audio: settings.audio_enabled,
1012 width: settings.width,
1013 height: settings.height,
1014 pixel_format: settings.pixel_format,
1015 sample_rate: settings.sample_rate,
1016 sample_format: settings.sample_format,
1017 channels: settings.channels.clone(),
1018 quality: settings.quality,
1019 ..Default::default()
1020 };
1021
1022 Self {
1023 renderer: TimelineRenderer::new(timeline, config),
1024 settings,
1025 }
1026 }
1027
1028 pub async fn export(&mut self) -> EditResult<Vec<RenderFrame>> {
1030 let mut frames = Vec::new();
1031 let start = self.settings.start.unwrap_or(0);
1032 let end = self.settings.end.unwrap_or(self.renderer.timeline.duration);
1033
1034 for position in start..end {
1035 let frame = self.renderer.render_frame_at(position).await?;
1036 frames.push(frame);
1037 }
1038
1039 Ok(frames)
1040 }
1041
1042 pub fn export_stream(&mut self) -> ExportStream {
1044 let start = self.settings.start.unwrap_or(0);
1045 let end = self.settings.end.unwrap_or(self.renderer.timeline.duration);
1046
1047 ExportStream {
1048 renderer: self.renderer.clone_for_stream(),
1049 current: start,
1050 end,
1051 }
1052 }
1053}
1054
1055#[derive(Clone, Debug)]
1057pub struct ExportSettings {
1058 pub video_enabled: bool,
1060 pub audio_enabled: bool,
1062 pub width: u32,
1064 pub height: u32,
1066 pub pixel_format: PixelFormat,
1068 pub sample_rate: u32,
1070 pub sample_format: SampleFormat,
1072 pub channels: ChannelLayout,
1074 pub quality: RenderQuality,
1076 pub start: Option<i64>,
1078 pub end: Option<i64>,
1080}
1081
1082impl Default for ExportSettings {
1083 fn default() -> Self {
1084 Self {
1085 video_enabled: true,
1086 audio_enabled: true,
1087 width: 1920,
1088 height: 1080,
1089 pixel_format: PixelFormat::Yuv420p,
1090 sample_rate: 48000,
1091 sample_format: SampleFormat::F32,
1092 channels: ChannelLayout::Stereo,
1093 quality: RenderQuality::High,
1094 start: None,
1095 end: None,
1096 }
1097 }
1098}
1099
1100pub struct ExportStream {
1102 renderer: TimelineRenderer,
1103 current: i64,
1104 end: i64,
1105}
1106
1107#[allow(dead_code)]
1111impl ExportStream {
1112 pub fn into_stream(self) -> impl futures::stream::Stream<Item = EditResult<RenderFrame>> {
1115 futures::stream::unfold(self, |mut state| async move {
1116 if state.current >= state.end {
1117 return None;
1118 }
1119 let position = state.current;
1120 state.current += 1;
1121 let result = state.renderer.render_frame_at(position).await;
1122 Some((result, state))
1123 })
1124 }
1125}
1126
1127impl TimelineRenderer {
1128 fn clone_for_stream(&self) -> Self {
1130 let max_pos = self.timeline.duration.max(0) as i64;
1131 let prefetch_config = PrefetchConfig::for_playback(30.0, 1.0);
1132 Self {
1133 timeline: self.timeline.clone(),
1134 config: self.config.clone(),
1135 cache: FrameCache::new(self.config.cache_size),
1136 raw_frame_cache: RawFrameCache::new(RAW_FRAME_CACHE_CAPACITY),
1137 source_cache: HashMap::new(),
1138 dirty_regions: Vec::new(),
1139 prefetch: PrefetchEngine::new(prefetch_config, max_pos),
1140 use_parallel: self.use_parallel,
1141 }
1142 }
1143}
1144
1145pub const RAW_FRAME_CACHE_CAPACITY: usize = 32;
1151
1152pub struct RawFrameCache {
1168 store: HashMap<u64, Vec<u8>>,
1170 order: VecDeque<u64>,
1172 capacity: usize,
1174}
1175
1176impl RawFrameCache {
1177 #[must_use]
1179 pub fn new(capacity: usize) -> Self {
1180 let capacity = capacity.max(1);
1181 Self {
1182 store: HashMap::with_capacity(capacity),
1183 order: VecDeque::with_capacity(capacity),
1184 capacity,
1185 }
1186 }
1187
1188 pub fn get_or_render(&mut self, frame_num: u64, render_fn: impl FnOnce() -> Vec<u8>) -> &[u8] {
1197 if !self.store.contains_key(&frame_num) {
1198 if self.store.len() >= self.capacity {
1200 if let Some(oldest) = self.order.pop_front() {
1201 self.store.remove(&oldest);
1202 }
1203 }
1204
1205 let data = render_fn();
1206 self.store.insert(frame_num, data);
1207 self.order.push_back(frame_num);
1208 }
1209
1210 self.store.get(&frame_num).map(Vec::as_slice).unwrap_or(&[])
1212 }
1213
1214 #[must_use]
1218 pub fn get(&self, frame_num: u64) -> Option<&[u8]> {
1219 self.store.get(&frame_num).map(Vec::as_slice)
1220 }
1221
1222 pub fn insert(&mut self, frame_num: u64, data: Vec<u8>) {
1227 if let std::collections::hash_map::Entry::Occupied(mut e) = self.store.entry(frame_num) {
1228 e.insert(data);
1229 return;
1230 }
1231 if self.store.len() >= self.capacity {
1232 if let Some(oldest) = self.order.pop_front() {
1233 self.store.remove(&oldest);
1234 }
1235 }
1236 self.store.insert(frame_num, data);
1237 self.order.push_back(frame_num);
1238 }
1239
1240 pub fn invalidate(&mut self, frame_num: u64) {
1242 if self.store.remove(&frame_num).is_some() {
1243 self.order.retain(|&f| f != frame_num);
1244 }
1245 }
1246
1247 pub fn clear(&mut self) {
1249 self.store.clear();
1250 self.order.clear();
1251 }
1252
1253 #[must_use]
1255 pub fn len(&self) -> usize {
1256 self.store.len()
1257 }
1258
1259 #[must_use]
1261 pub fn is_empty(&self) -> bool {
1262 self.store.is_empty()
1263 }
1264
1265 #[must_use]
1267 pub fn capacity(&self) -> usize {
1268 self.capacity
1269 }
1270
1271 #[must_use]
1273 pub fn contains(&self, frame_num: u64) -> bool {
1274 self.store.contains_key(&frame_num)
1275 }
1276}
1277
1278impl std::fmt::Debug for RawFrameCache {
1279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1280 f.debug_struct("RawFrameCache")
1281 .field("len", &self.store.len())
1282 .field("capacity", &self.capacity)
1283 .finish()
1284 }
1285}
1286
1287#[cfg(test)]
1292mod raw_cache_tests {
1293 use super::{RawFrameCache, RAW_FRAME_CACHE_CAPACITY};
1294
1295 #[test]
1296 fn test_raw_frame_cache_basic_get_or_render() {
1297 let mut cache = RawFrameCache::new(4);
1298 let mut render_count = 0usize;
1299
1300 let data = cache.get_or_render(0, || {
1302 render_count += 1;
1303 vec![1u8, 2, 3]
1304 });
1305 assert_eq!(data, &[1u8, 2, 3]);
1306 assert_eq!(render_count, 1);
1307
1308 let data2 = cache.get_or_render(0, || {
1310 render_count += 1;
1311 vec![99u8]
1312 });
1313 assert_eq!(data2, &[1u8, 2, 3]);
1314 assert_eq!(render_count, 1, "render_fn should not be called twice");
1315 }
1316
1317 #[test]
1318 fn test_raw_frame_cache_lru_eviction() {
1319 let mut cache = RawFrameCache::new(4);
1320
1321 for i in 0u64..4 {
1323 cache.get_or_render(i, || vec![i as u8]);
1324 }
1325 assert_eq!(cache.len(), 4);
1326
1327 cache.get_or_render(4, || vec![4u8]);
1329 assert_eq!(cache.len(), 4, "cache must not exceed capacity");
1330 assert!(
1331 !cache.contains(0),
1332 "oldest frame (0) should have been evicted"
1333 );
1334 assert!(
1335 cache.contains(4),
1336 "newly inserted frame (4) should be present"
1337 );
1338 }
1339
1340 #[test]
1341 fn test_raw_frame_cache_capacity_32() {
1342 let cache = RawFrameCache::new(RAW_FRAME_CACHE_CAPACITY);
1343 assert_eq!(cache.capacity(), 32);
1344 }
1345
1346 #[test]
1347 fn test_raw_frame_cache_get_missing() {
1348 let cache = RawFrameCache::new(4);
1349 assert!(cache.get(99).is_none());
1350 }
1351
1352 #[test]
1353 fn test_raw_frame_cache_insert_and_get() {
1354 let mut cache = RawFrameCache::new(4);
1355 cache.insert(7, vec![10, 20, 30]);
1356 assert_eq!(cache.get(7), Some(&[10u8, 20, 30][..]));
1357 }
1358
1359 #[test]
1360 fn test_raw_frame_cache_invalidate() {
1361 let mut cache = RawFrameCache::new(4);
1362 cache.insert(1, vec![1, 2]);
1363 cache.invalidate(1);
1364 assert!(!cache.contains(1));
1365 assert_eq!(cache.len(), 0);
1366 }
1367
1368 #[test]
1369 fn test_raw_frame_cache_clear() {
1370 let mut cache = RawFrameCache::new(4);
1371 for i in 0u64..4 {
1372 cache.insert(i, vec![i as u8]);
1373 }
1374 cache.clear();
1375 assert!(cache.is_empty());
1376 assert_eq!(cache.len(), 0);
1377 }
1378
1379 #[test]
1380 fn test_raw_frame_cache_eviction_order() {
1381 let mut cache = RawFrameCache::new(3);
1382 cache.insert(10, vec![10]);
1383 cache.insert(20, vec![20]);
1384 cache.insert(30, vec![30]);
1385
1386 cache.insert(40, vec![40]);
1388 assert!(!cache.contains(10));
1389 assert!(cache.contains(20));
1390 assert!(cache.contains(30));
1391 assert!(cache.contains(40));
1392
1393 cache.insert(50, vec![50]);
1395 assert!(!cache.contains(20));
1396 assert!(cache.contains(30));
1397 assert!(cache.contains(40));
1398 assert!(cache.contains(50));
1399 }
1400
1401 #[test]
1402 fn test_raw_frame_cache_capacity_clamped_to_one() {
1403 let cache = RawFrameCache::new(0);
1404 assert_eq!(cache.capacity(), 1);
1405 }
1406
1407 #[test]
1408 fn test_raw_frame_cache_is_empty_initially() {
1409 let cache = RawFrameCache::new(8);
1410 assert!(cache.is_empty());
1411 }
1412
1413 #[test]
1414 fn test_raw_frame_cache_debug_format() {
1415 let cache = RawFrameCache::new(4);
1416 let debug = format!("{cache:?}");
1417 assert!(debug.contains("RawFrameCache"), "debug output: {debug}");
1418 }
1419}
1420
1421pub struct TransitionRenderer;
1427
1428impl TransitionRenderer {
1429 #[must_use]
1435 pub fn blend_video(
1436 frame_a: &VideoFrame,
1437 frame_b: &VideoFrame,
1438 transition: &Transition,
1439 progress: f64,
1440 ) -> VideoFrame {
1441 use crate::transition::TransitionType;
1442
1443 if frame_a.format != frame_b.format {
1445 return frame_a.clone();
1446 }
1447 let a_pixels = frame_a.width as u64 * frame_a.height as u64;
1448 let b_pixels = frame_b.width as u64 * frame_b.height as u64;
1449 if a_pixels != b_pixels {
1450 return if b_pixels > a_pixels {
1451 frame_b.clone()
1452 } else {
1453 frame_a.clone()
1454 };
1455 }
1456
1457 let progress_f32 = progress as f32;
1458
1459 match &transition.transition_type {
1460 TransitionType::Dissolve => Self::dissolve_video(frame_a, frame_b, progress_f32),
1461 TransitionType::WipeLeft => {
1462 Self::wipe_video(frame_a, frame_b, progress_f32, WipeDirection::Left)
1463 }
1464 TransitionType::WipeRight => {
1465 Self::wipe_video(frame_a, frame_b, progress_f32, WipeDirection::Right)
1466 }
1467 TransitionType::WipeDown => {
1468 Self::wipe_video(frame_a, frame_b, progress_f32, WipeDirection::Down)
1469 }
1470 TransitionType::WipeUp => {
1471 Self::wipe_video(frame_a, frame_b, progress_f32, WipeDirection::Up)
1472 }
1473 _ => {
1476 if progress_f32 >= 0.5 {
1477 frame_b.clone()
1478 } else {
1479 frame_a.clone()
1480 }
1481 }
1482 }
1483 }
1484
1485 #[must_use]
1492 pub fn mix_audio(
1493 frame_a: &AudioFrame,
1494 frame_b: &AudioFrame,
1495 _transition: &Transition,
1496 progress: f64,
1497 ) -> AudioFrame {
1498 if frame_a.format != frame_b.format {
1500 return frame_a.clone();
1501 }
1502
1503 let alpha = progress as f32;
1504 let inv_alpha = 1.0_f32 - alpha;
1505
1506 match (&frame_a.samples, &frame_b.samples) {
1507 (AudioBuffer::Interleaved(a_bytes), AudioBuffer::Interleaved(b_bytes))
1508 if frame_a.format == SampleFormat::F32 =>
1509 {
1510 let len_samples = (a_bytes.len() / 4).min(b_bytes.len() / 4);
1511 let mut out_bytes = Vec::with_capacity(len_samples * 4);
1512
1513 for i in 0..len_samples {
1514 let base = i * 4;
1515 let a_val = f32::from_ne_bytes([
1516 a_bytes[base],
1517 a_bytes[base + 1],
1518 a_bytes[base + 2],
1519 a_bytes[base + 3],
1520 ]);
1521 let b_val = f32::from_ne_bytes([
1522 b_bytes[base],
1523 b_bytes[base + 1],
1524 b_bytes[base + 2],
1525 b_bytes[base + 3],
1526 ]);
1527 let blended = (a_val * inv_alpha + b_val * alpha).clamp(-1.0, 1.0);
1528 out_bytes.extend_from_slice(&blended.to_ne_bytes());
1529 }
1530
1531 AudioFrame {
1532 format: frame_a.format,
1533 sample_rate: frame_a.sample_rate,
1534 channels: frame_a.channels.clone(),
1535 samples: AudioBuffer::Interleaved(Bytes::from(out_bytes)),
1536 timestamp: frame_a.timestamp,
1537 }
1538 }
1539 (AudioBuffer::Planar(a_planes), AudioBuffer::Planar(b_planes))
1540 if frame_a.format == SampleFormat::F32p =>
1541 {
1542 let plane_count = a_planes.len().min(b_planes.len());
1543 let mut out_planes = Vec::with_capacity(plane_count);
1544
1545 for p in 0..plane_count {
1546 let a_plane = &a_planes[p];
1547 let b_plane = &b_planes[p];
1548 let len_samples = (a_plane.len() / 4).min(b_plane.len() / 4);
1549 let mut plane_bytes = Vec::with_capacity(len_samples * 4);
1550
1551 for i in 0..len_samples {
1552 let base = i * 4;
1553 let a_val = f32::from_ne_bytes([
1554 a_plane[base],
1555 a_plane[base + 1],
1556 a_plane[base + 2],
1557 a_plane[base + 3],
1558 ]);
1559 let b_val = f32::from_ne_bytes([
1560 b_plane[base],
1561 b_plane[base + 1],
1562 b_plane[base + 2],
1563 b_plane[base + 3],
1564 ]);
1565 let blended = (a_val * inv_alpha + b_val * alpha).clamp(-1.0, 1.0);
1566 plane_bytes.extend_from_slice(&blended.to_ne_bytes());
1567 }
1568
1569 out_planes.push(Bytes::from(plane_bytes));
1570 }
1571
1572 AudioFrame {
1573 format: frame_a.format,
1574 sample_rate: frame_a.sample_rate,
1575 channels: frame_a.channels.clone(),
1576 samples: AudioBuffer::Planar(out_planes),
1577 timestamp: frame_a.timestamp,
1578 }
1579 }
1580 _ => frame_a.clone(),
1582 }
1583 }
1584
1585 fn dissolve_video(frame_a: &VideoFrame, frame_b: &VideoFrame, progress: f32) -> VideoFrame {
1589 use oximedia_codec::frame::Plane;
1590
1591 let inv = 1.0_f32 - progress;
1592 let mut output = frame_a.clone();
1593
1594 for (out_plane, b_plane) in output.planes.iter_mut().zip(frame_b.planes.iter()) {
1595 let len = out_plane.data.len().min(b_plane.data.len());
1596 let blended: Vec<u8> = (0..len)
1597 .map(|i| {
1598 #[allow(clippy::cast_possible_truncation)]
1599 #[allow(clippy::cast_sign_loss)]
1600 let v = (out_plane.data[i] as f32 * inv + b_plane.data[i] as f32 * progress)
1601 .round()
1602 .clamp(0.0, 255.0) as u8;
1603 v
1604 })
1605 .collect();
1606
1607 let new_plane = Plane::with_dimensions(
1610 blended,
1611 out_plane.stride,
1612 out_plane.width,
1613 out_plane.height,
1614 );
1615 *out_plane = new_plane;
1616 }
1617
1618 output
1619 }
1620
1621 fn wipe_video(
1623 frame_a: &VideoFrame,
1624 frame_b: &VideoFrame,
1625 progress: f32,
1626 direction: WipeDirection,
1627 ) -> VideoFrame {
1628 use oximedia_codec::frame::Plane;
1629
1630 let mut output = frame_a.clone();
1631
1632 for (out_plane, b_plane) in output.planes.iter_mut().zip(frame_b.planes.iter()) {
1634 let pw = out_plane.width as usize;
1635 let ph = out_plane.height as usize;
1636 let mut new_data = out_plane.data.clone();
1640
1641 match direction {
1642 WipeDirection::Left | WipeDirection::Right => {
1643 #[allow(clippy::cast_possible_truncation)]
1645 #[allow(clippy::cast_sign_loss)]
1646 let boundary = (progress * pw as f32).round() as usize;
1647 for y in 0..ph {
1648 for x in 0..pw {
1649 let use_b = match direction {
1650 WipeDirection::Left => x < boundary,
1651 WipeDirection::Right => x >= pw.saturating_sub(boundary),
1652 _ => false,
1653 };
1654 if use_b {
1655 let src_idx = y * b_plane.stride + x;
1656 let dst_idx = y * out_plane.stride + x;
1657 if src_idx < b_plane.data.len() && dst_idx < new_data.len() {
1658 new_data[dst_idx] = b_plane.data[src_idx];
1659 }
1660 }
1661 }
1662 }
1663 }
1664 WipeDirection::Down | WipeDirection::Up => {
1665 #[allow(clippy::cast_possible_truncation)]
1666 #[allow(clippy::cast_sign_loss)]
1667 let boundary = (progress * ph as f32).round() as usize;
1668 for y in 0..ph {
1669 let use_b = match direction {
1670 WipeDirection::Down => y < boundary,
1671 WipeDirection::Up => y >= ph.saturating_sub(boundary),
1672 _ => false,
1673 };
1674 if use_b {
1675 for x in 0..pw {
1676 let src_idx = y * b_plane.stride + x;
1677 let dst_idx = y * out_plane.stride + x;
1678 if src_idx < b_plane.data.len() && dst_idx < new_data.len() {
1679 new_data[dst_idx] = b_plane.data[src_idx];
1680 }
1681 }
1682 }
1683 }
1684 }
1685 }
1686
1687 let new_plane = Plane::with_dimensions(
1688 new_data,
1689 out_plane.stride,
1690 out_plane.width,
1691 out_plane.height,
1692 );
1693 *out_plane = new_plane;
1694 }
1695
1696 output
1697 }
1698}
1699
1700#[derive(Clone, Copy)]
1702enum WipeDirection {
1703 Left,
1704 Right,
1705 Down,
1706 Up,
1707}
1708
1709#[cfg(test)]
1714mod transition_renderer_tests {
1715 use super::*;
1716 use crate::transition::{Transition, TransitionType};
1717 use bytes::Bytes;
1718 use oximedia_audio::{AudioBuffer, AudioFrame, ChannelLayout};
1719 use oximedia_codec::VideoFrame;
1720 use oximedia_core::{PixelFormat, SampleFormat};
1721
1722 fn make_video_frame(width: u32, height: u32, value: u8) -> VideoFrame {
1724 let mut frame = VideoFrame::new(PixelFormat::Yuv420p, width, height);
1725 frame.allocate();
1726 for plane in &mut frame.planes {
1727 for b in &mut plane.data {
1728 *b = value;
1729 }
1730 }
1731 frame
1732 }
1733
1734 fn make_audio_frame(num_samples: usize, value: f32) -> AudioFrame {
1736 let bytes: Vec<u8> = (0..num_samples).flat_map(|_| value.to_ne_bytes()).collect();
1737 AudioFrame {
1738 format: SampleFormat::F32,
1739 sample_rate: 48_000,
1740 channels: ChannelLayout::Stereo,
1741 samples: AudioBuffer::Interleaved(Bytes::from(bytes)),
1742 timestamp: oximedia_core::Timestamp::new(0, oximedia_core::Rational::new(1, 48_000)),
1743 }
1744 }
1745
1746 fn make_transition(tt: TransitionType) -> Transition {
1748 Transition::new(0, tt, 0, 0, 1000, 0, 1)
1749 }
1750
1751 #[test]
1755 fn test_blend_video_dissolve_mid() {
1756 let frame_a = make_video_frame(8, 4, 100);
1757 let frame_b = make_video_frame(8, 4, 200);
1758 let t = make_transition(TransitionType::Dissolve);
1759
1760 let out = TransitionRenderer::blend_video(&frame_a, &frame_b, &t, 0.5);
1761
1762 for plane in &out.planes {
1763 for &b in &plane.data {
1764 assert!((i32::from(b) - 150).abs() <= 1, "expected ~150, got {b}");
1765 }
1766 }
1767 }
1768
1769 #[test]
1771 fn test_blend_video_dissolve_zero() {
1772 let frame_a = make_video_frame(8, 4, 80);
1773 let frame_b = make_video_frame(8, 4, 200);
1774 let t = make_transition(TransitionType::Dissolve);
1775
1776 let out = TransitionRenderer::blend_video(&frame_a, &frame_b, &t, 0.0);
1777
1778 for plane in &out.planes {
1779 for &b in &plane.data {
1780 assert_eq!(b, 80);
1781 }
1782 }
1783 }
1784
1785 #[test]
1787 fn test_blend_video_dissolve_one() {
1788 let frame_a = make_video_frame(8, 4, 80);
1789 let frame_b = make_video_frame(8, 4, 200);
1790 let t = make_transition(TransitionType::Dissolve);
1791
1792 let out = TransitionRenderer::blend_video(&frame_a, &frame_b, &t, 1.0);
1793
1794 for plane in &out.planes {
1795 for &b in &plane.data {
1796 assert_eq!(b, 200);
1797 }
1798 }
1799 }
1800
1801 #[test]
1803 fn test_blend_video_dimension_mismatch_no_panic() {
1804 let frame_a = make_video_frame(8, 4, 100);
1805 let frame_b = make_video_frame(16, 8, 200);
1806 let t = make_transition(TransitionType::Dissolve);
1807
1808 let out = TransitionRenderer::blend_video(&frame_a, &frame_b, &t, 0.5);
1810 assert_eq!(out.width, 16);
1812 assert_eq!(out.height, 8);
1813 }
1814
1815 #[test]
1817 fn test_blend_video_dimension_mismatch_a_larger() {
1818 let frame_a = make_video_frame(16, 8, 100);
1819 let frame_b = make_video_frame(8, 4, 200);
1820 let t = make_transition(TransitionType::Dissolve);
1821
1822 let out = TransitionRenderer::blend_video(&frame_a, &frame_b, &t, 0.5);
1823 assert_eq!(out.width, 16);
1824 assert_eq!(out.height, 8);
1825 }
1826
1827 #[test]
1829 fn test_blend_video_wipe_left() {
1830 let frame_a = make_video_frame(8, 4, 10);
1831 let frame_b = make_video_frame(8, 4, 250);
1832 let t = make_transition(TransitionType::WipeLeft);
1833
1834 let out = TransitionRenderer::blend_video(&frame_a, &frame_b, &t, 0.5);
1835
1836 let y_plane = &out.planes[0];
1838 for y in 0..4usize {
1839 for x in 0..4usize {
1840 assert_eq!(y_plane.data[y * y_plane.stride + x], 250, "x={x},y={y}");
1841 }
1842 for x in 4..8usize {
1843 assert_eq!(y_plane.data[y * y_plane.stride + x], 10, "x={x},y={y}");
1844 }
1845 }
1846 }
1847
1848 #[test]
1852 fn test_mix_audio_f32_mid() {
1853 let frame_a = make_audio_frame(64, 0.5_f32);
1854 let frame_b = make_audio_frame(64, -0.5_f32);
1855 let t = make_transition(TransitionType::CrossFade);
1856
1857 let out = TransitionRenderer::mix_audio(&frame_a, &frame_b, &t, 0.5);
1858
1859 if let AudioBuffer::Interleaved(bytes) = &out.samples {
1860 for chunk in bytes.chunks_exact(4) {
1861 let v = f32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
1862 assert!(v.abs() < 1e-5, "expected ~0.0, got {v}");
1863 }
1864 } else {
1865 panic!("expected interleaved buffer");
1866 }
1867 }
1868
1869 #[test]
1871 fn test_mix_audio_f32_zero() {
1872 let frame_a = make_audio_frame(32, 0.8_f32);
1873 let frame_b = make_audio_frame(32, -0.8_f32);
1874 let t = make_transition(TransitionType::CrossFade);
1875
1876 let out = TransitionRenderer::mix_audio(&frame_a, &frame_b, &t, 0.0);
1877
1878 if let AudioBuffer::Interleaved(bytes) = &out.samples {
1879 for chunk in bytes.chunks_exact(4) {
1880 let v = f32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
1881 assert!((v - 0.8).abs() < 1e-5, "expected 0.8, got {v}");
1882 }
1883 } else {
1884 panic!("expected interleaved buffer");
1885 }
1886 }
1887
1888 #[test]
1890 fn test_mix_audio_f32_one() {
1891 let frame_a = make_audio_frame(32, 0.3_f32);
1892 let frame_b = make_audio_frame(32, 0.9_f32);
1893 let t = make_transition(TransitionType::CrossFade);
1894
1895 let out = TransitionRenderer::mix_audio(&frame_a, &frame_b, &t, 1.0);
1896
1897 if let AudioBuffer::Interleaved(bytes) = &out.samples {
1898 for chunk in bytes.chunks_exact(4) {
1899 let v = f32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
1900 assert!((v - 0.9).abs() < 1e-5, "expected 0.9, got {v}");
1901 }
1902 } else {
1903 panic!("expected interleaved buffer");
1904 }
1905 }
1906
1907 #[test]
1909 fn test_mix_audio_format_mismatch() {
1910 let frame_a = make_audio_frame(16, 0.5_f32);
1911 let mut frame_b = make_audio_frame(16, -0.5_f32);
1912 frame_b.format = SampleFormat::S16; let t = make_transition(TransitionType::CrossFade);
1915 let out = TransitionRenderer::mix_audio(&frame_a, &frame_b, &t, 0.5);
1916
1917 assert_eq!(out.format, SampleFormat::F32);
1919 assert_eq!(out.samples.size(), frame_a.samples.size());
1920 }
1921}