Skip to main content

ass_renderer/debug/player/
mod.rs

1use crate::{BackendType, Frame, RenderContext, RenderError, Renderer};
2use ass_core::parser::Script;
3
4#[cfg(not(feature = "nostd"))]
5use std::collections::HashMap;
6#[cfg(not(feature = "nostd"))]
7use std::time::Instant;
8
9#[cfg(feature = "nostd")]
10use alloc::collections::BTreeMap as HashMap;
11#[cfg(feature = "nostd")]
12use alloc::string::String;
13
14mod playback;
15mod render;
16mod report;
17mod testing;
18
19pub use report::{PlayerFrame, TestPoint, TestReport};
20
21/// Debug player for visual verification of subtitle rendering
22pub struct DebugPlayer {
23    renderer: Renderer,
24    script_content: Option<String>,
25    #[allow(dead_code)] // Debug feature - may be used for future player functionality
26    parsed_script: Option<Script<'static>>,
27    current_time_ms: u32,
28    playback_speed: f32,
29    is_playing: bool,
30    frame_interval_ms: u32,
31    output_dir: String,
32    save_frames: bool,
33    show_stats: bool,
34    loop_playback: bool,
35    start_time_ms: u32,
36    end_time_ms: u32,
37    frame_cache: HashMap<u32, Frame>,
38    cache_enabled: bool,
39    max_cache_size: usize,
40    // Timing fields for proper synchronization
41    playback_start_instant: Option<Instant>,
42    playback_start_time_ms: u32,
43    accumulated_time_ms: f32,
44    width: u32,
45    height: u32,
46}
47
48impl DebugPlayer {
49    pub fn new(backend_type: BackendType, width: u32, height: u32) -> Result<Self, RenderError> {
50        let mut context = RenderContext::new(width, height);
51        context.font_database_mut().load_system_fonts();
52
53        let renderer = Renderer::new(backend_type, context)?;
54
55        Ok(Self {
56            renderer,
57            script_content: None,
58            parsed_script: None,
59            current_time_ms: 0,
60            playback_speed: 1.0,
61            is_playing: false,
62            frame_interval_ms: 40, // ~25 FPS for subtitle granularity
63            output_dir: "debug_player_output".to_string(),
64            save_frames: false,
65            show_stats: true,
66            loop_playback: false,
67            start_time_ms: 0,
68            end_time_ms: 0, // Default to 0, will be set when script is loaded
69            frame_cache: HashMap::new(),
70            cache_enabled: true,
71            max_cache_size: 100, // Cache up to 100 frames
72            playback_start_instant: None,
73            playback_start_time_ms: 0,
74            accumulated_time_ms: 0.0,
75            width,
76            height,
77        })
78    }
79
80    pub fn width(&self) -> u32 {
81        self.width
82    }
83
84    pub fn height(&self) -> u32 {
85        self.height
86    }
87
88    /// Check if a script is currently loaded
89    pub fn has_script(&self) -> bool {
90        self.script_content.is_some()
91    }
92
93    /// Get the current end time in milliseconds
94    pub fn duration_ms(&self) -> u32 {
95        self.end_time_ms
96    }
97
98    pub fn load_script(&mut self, script_content: &str) -> Result<(), RenderError> {
99        // Parse and store the script to avoid re-parsing on every frame
100        let owned_content = script_content.to_string();
101        let script = Script::parse(&owned_content)
102            .map_err(|e| RenderError::ParseError(format!("Failed to parse script: {e:?}")))?;
103
104        // Calculate actual end time from events
105        let mut max_time = 0u32;
106        let mut event_count = 0;
107        for section in script.sections() {
108            if let ass_core::parser::Section::Events(events) = section {
109                for event in events.iter() {
110                    event_count += 1;
111                    if let Ok(end) = event.end_time_cs() {
112                        max_time = max_time.max(end * 10); // Convert centiseconds to milliseconds
113                    }
114                }
115            }
116        }
117
118        if max_time > 0 {
119            self.end_time_ms = max_time;
120        } else {
121            // If no events found or all events have no end time,
122            // set a reasonable default duration (10 seconds)
123            self.end_time_ms = 10000;
124        }
125
126        // Clear cache when loading new script
127        self.frame_cache.clear();
128
129        // Store both the content and parsed script
130        self.script_content = Some(owned_content);
131        // Note: We can't store the parsed script due to lifetime issues
132        // We'll need to parse on demand but cache rendered frames
133
134        println!(
135            "Script loaded. Duration: {duration}ms, Events: {events}",
136            duration = self.end_time_ms,
137            events = event_count
138        );
139
140        Ok(())
141    }
142}