1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
//! Encoding dialog UI
//!
//! Provides dialog for configuring and running video encoding.
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{channel, Receiver};
use std::sync::Arc;
use std::thread::JoinHandle;
use eframe::egui;
use log::info;
use crate::cache::Cache;
use crate::encode::{
Container, EncodeError, EncodeProgress, EncodeStage, EncoderImpl, EncoderSettings,
QualityMode, VideoCodec,
};
use crate::progress_bar::ProgressBar;
/// Encoding dialog state
pub struct EncodeDialog {
/// Current encoder settings (editable)
pub settings: EncoderSettings,
/// Whether encoding is currently in progress
pub is_encoding: bool,
/// Current encoding progress (if encoding)
pub progress: Option<EncodeProgress>,
/// Cancel flag shared with encoder thread
pub cancel_flag: Arc<AtomicBool>,
/// Channel receiver for progress updates
progress_rx: Option<Receiver<EncodeProgress>>,
/// Encoder thread handle
encode_thread: Option<JoinHandle<Result<(), EncodeError>>>,
/// Progress bar widget
progress_bar: ProgressBar,
/// Last encoder name (for display)
encoder_name: String,
}
impl EncodeDialog {
/// Create new encode dialog with settings from AppSettings
pub fn new(settings: EncoderSettings) -> Self {
Self {
settings,
is_encoding: false,
progress: None,
cancel_flag: Arc::new(AtomicBool::new(false)),
progress_rx: None,
encode_thread: None,
progress_bar: ProgressBar::new(400.0, 20.0),
encoder_name: String::new(),
}
}
/// Render the encode dialog
///
/// Returns: true if dialog should remain open, false if closed
pub fn render(&mut self, ctx: &egui::Context, cache: &Cache) -> bool {
let mut should_close = false;
// Poll progress updates
if let Some(rx) = &self.progress_rx {
while let Ok(progress) = rx.try_recv() {
self.progress = Some(progress);
}
}
// Check if encoding completed
if let Some(ref progress) = self.progress {
match &progress.stage {
EncodeStage::Complete => {
info!("Encoding completed successfully");
self.stop_encoding();
}
EncodeStage::Error(msg) => {
info!("Encoding failed: {}", msg);
self.stop_encoding();
}
_ => {}
}
}
egui::Window::new("Video Encoder")
.resizable(false)
.collapsible(false)
.show(ctx, |ui| {
ui.set_width(500.0);
// === Output Path ===
ui.horizontal(|ui| {
ui.label("Output:");
ui.add_enabled_ui(!self.is_encoding, |ui| {
let path_str = self.settings.output_path.display().to_string();
let mut edit_path = path_str.clone();
if ui.text_edit_singleline(&mut edit_path).changed() {
self.settings.output_path = PathBuf::from(edit_path);
}
if ui.button("Browse").clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_file_name("output.mp4")
.save_file()
{
self.settings.output_path = path;
}
}
});
});
ui.add_space(8.0);
// === Container ===
ui.label("Container:");
ui.horizontal(|ui| {
ui.add_enabled_ui(!self.is_encoding, |ui| {
for container in Container::all() {
if ui
.radio_value(&mut self.settings.container, *container, container.to_string())
.changed()
{
// Update file extension when container changes
self.settings
.output_path
.set_extension(container.extension());
}
}
});
});
ui.add_space(8.0);
// === Codec ===
ui.label("Codec:");
ui.horizontal(|ui| {
ui.add_enabled_ui(!self.is_encoding, |ui| {
for codec in VideoCodec::all() {
ui.radio_value(&mut self.settings.codec, *codec, codec.to_string());
}
});
});
ui.add_space(8.0);
// === Encoder Implementation ===
ui.label("Encoder:");
ui.add_enabled_ui(!self.is_encoding, |ui| {
for impl_type in EncoderImpl::all() {
ui.radio_value(
&mut self.settings.encoder_impl,
*impl_type,
impl_type.to_string(),
);
}
});
ui.add_space(8.0);
// === Quality Mode ===
ui.label("Quality:");
ui.horizontal(|ui| {
ui.add_enabled_ui(!self.is_encoding, |ui| {
for mode in QualityMode::all() {
ui.radio_value(&mut self.settings.quality_mode, *mode, mode.to_string());
}
});
});
ui.horizontal(|ui| {
ui.label("Value:");
ui.add_enabled_ui(!self.is_encoding, |ui| {
let hint = match self.settings.quality_mode {
QualityMode::CRF => "18=best, 23=default, 28=fast",
QualityMode::Bitrate => "kbps",
};
ui.add(
egui::Slider::new(&mut self.settings.quality_value, 1..=10000)
.text(hint),
);
});
});
ui.horizontal(|ui| {
ui.label("Framerate:");
ui.add_enabled_ui(!self.is_encoding, |ui| {
ui.add(
egui::Slider::new(&mut self.settings.fps, 1.0..=240.0)
.text("fps"),
);
});
});
ui.add_space(8.0);
// === Frame Range Info ===
let (start, end) = cache.get_play_range();
let frame_count = end.saturating_sub(start) + 1;
ui.label(format!(
"Frame Range: {} - {} ({} frames)",
start, end, frame_count
));
ui.add_space(12.0);
// === Progress Section (only when encoding) ===
if self.is_encoding {
ui.separator();
ui.heading("Progress");
if let Some(ref progress) = self.progress {
// Stage description
let stage_text = match &progress.stage {
EncodeStage::Validating => "Validating frame sizes...",
EncodeStage::Opening => "Opening encoder...",
EncodeStage::Encoding => "Encoding frames...",
EncodeStage::Flushing => "Flushing encoder...",
EncodeStage::Complete => "Complete!",
EncodeStage::Error(msg) => msg.as_str(),
};
ui.label(stage_text);
// Progress bar
self.progress_bar
.set_progress(progress.current_frame, progress.total_frames);
self.progress_bar.render(ui);
// Encoder name
if !self.encoder_name.is_empty() {
ui.label(format!("Encoder: {}", self.encoder_name));
}
}
ui.add_space(8.0);
}
ui.separator();
// === Buttons ===
ui.horizontal(|ui| {
// Close/Cancel button
let close_text = if self.is_encoding { "Cancel" } else { "Close" };
if ui.button(close_text).clicked() {
if self.is_encoding {
self.cancel_encoding();
}
should_close = true;
}
// Encode button (disabled during encoding)
ui.add_enabled_ui(!self.is_encoding, |ui| {
if ui.button("Encode").clicked() {
self.start_encoding(cache);
}
});
});
});
// Return true if window should stay open
!should_close
}
/// Start encoding process
fn start_encoding(&mut self, cache: &Cache) {
info!("Starting encoding: {:?}", self.settings);
// Reset cancel flag
self.cancel_flag.store(false, Ordering::Relaxed);
// Create progress channel
let (tx, rx) = channel();
self.progress_rx = Some(rx);
// Clone data for thread (including play_range)
let cache_clone = cache.sequences().iter()
.cloned()
.collect::<Vec<_>>();
let play_range = cache.get_play_range();
let settings_clone = self.settings.clone();
let cancel_flag_clone = Arc::clone(&self.cancel_flag);
// Spawn encoder thread
use crate::encode::encode_sequence;
use std::thread;
let handle = thread::spawn(move || {
// Create temporary cache with cloned sequences
let (mut temp_cache, _rx) = Cache::new(0.75, None);
for seq in cache_clone {
temp_cache.append_seq(seq);
}
// Set play range from original cache (append_seq sets full range by default)
temp_cache.set_play_range(play_range.0, play_range.1);
// Run encoding
encode_sequence(&mut temp_cache, &settings_clone, tx, cancel_flag_clone)
});
self.encode_thread = Some(handle);
self.is_encoding = true;
}
/// Cancel encoding
fn cancel_encoding(&mut self) {
info!("Cancelling encoding");
self.cancel_flag.store(true, Ordering::Relaxed);
}
/// Stop encoding (cleanup after completion or error)
fn stop_encoding(&mut self) {
self.is_encoding = false;
self.progress_rx = None;
self.encode_thread = None;
}
}