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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
//! Video recording functionality.
use std::path::Path;
use image::{ImageBuffer, Rgb};
use super::Window;
/// Configuration options for video recording.
///
/// Use this to customize recording behavior such as frame skipping.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RecordingConfig {
/// Record every Nth frame. Set to 1 to record every frame,
/// 2 to record every other frame, etc.
/// Default: 1
pub frame_skip: u32,
}
impl Default for RecordingConfig {
fn default() -> Self {
Self { frame_skip: 1 }
}
}
impl RecordingConfig {
/// Creates a new recording config with default settings (every frame).
pub fn new() -> Self {
Self::default()
}
/// Sets how many frames to skip between captures.
/// 1 = every frame, 2 = every other frame, etc.
pub fn with_frame_skip(mut self, skip: u32) -> Self {
self.frame_skip = skip.max(1);
self
}
}
/// State for video recording.
pub(crate) struct RecordingState {
pub(crate) frames: Vec<ImageBuffer<Rgb<u8>, Vec<u8>>>,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) config: RecordingConfig,
pub(crate) paused: bool,
pub(crate) frame_counter: u32,
}
impl Window {
/// Starts recording frames for a screencast with default settings.
///
/// After calling this method, each frame rendered will be captured and stored.
/// Call `end_recording` to stop recording and encode the frames to an MP4 video file.
///
/// **Note:** This feature requires the `recording` feature to be enabled.
///
/// # Example
/// ```no_run
/// # use kiss3d::window::Window;
/// # #[kiss3d::main]
/// # async fn main() {
/// # let mut window = Window::new("Example").await;
/// window.begin_recording();
/// // Render some frames...
/// # for _ in 0..60 {
/// # window.render().await;
/// # }
/// window.end_recording("output.mp4", 30).unwrap();
/// # }
/// ```
pub fn begin_recording(&mut self) {
self.begin_recording_with_config(RecordingConfig::default());
}
/// Starts recording frames for a screencast with custom configuration.
///
/// # Arguments
/// * `config` - Recording configuration specifying frame skip, etc.
///
/// # Example
/// ```no_run
/// # use kiss3d::window::{Window, RecordingConfig};
/// # #[kiss3d::main]
/// # async fn main() {
/// # let mut window = Window::new("Example").await;
/// // Record every 2nd frame (reduces file size and encoding time)
/// let config = RecordingConfig::new()
/// .with_frame_skip(2);
/// window.begin_recording_with_config(config);
/// # for _ in 0..60 {
/// # window.render().await;
/// # }
/// window.end_recording("output.mp4", 30).unwrap();
/// # }
/// ```
pub fn begin_recording_with_config(&mut self, config: RecordingConfig) {
let (width, height) = self.canvas.size();
self.recording = Some(RecordingState {
frames: Vec::new(),
width,
height,
config,
paused: false,
frame_counter: 0,
});
}
/// Returns whether recording is currently active.
///
/// **Note:** This feature requires the `recording` feature to be enabled.
pub fn is_recording(&self) -> bool {
self.recording.is_some()
}
/// Returns whether recording is currently paused.
///
/// **Note:** This feature requires the `recording` feature to be enabled.
pub fn is_recording_paused(&self) -> bool {
self.recording.as_ref().is_some_and(|r| r.paused)
}
/// Pauses the current recording.
///
/// While paused, frames will not be captured. Call `resume_recording` to continue.
///
/// # Example
/// ```no_run
/// # use kiss3d::window::Window;
/// # #[kiss3d::main]
/// # async fn main() {
/// # let mut window = Window::new("Example").await;
/// window.begin_recording();
/// // Record some frames...
/// # for _ in 0..30 { window.render().await; }
/// window.pause_recording();
/// // These frames won't be recorded
/// # for _ in 0..30 { window.render().await; }
/// window.resume_recording();
/// // Continue recording...
/// # for _ in 0..30 { window.render().await; }
/// window.end_recording("output.mp4", 30).unwrap();
/// # }
/// ```
pub fn pause_recording(&mut self) {
if let Some(ref mut recording) = self.recording {
recording.paused = true;
}
}
/// Resumes a paused recording.
///
/// # Example
/// ```no_run
/// # use kiss3d::window::Window;
/// # #[kiss3d::main]
/// # async fn main() {
/// # let mut window = Window::new("Example").await;
/// window.begin_recording();
/// window.pause_recording();
/// // ... do something without recording ...
/// window.resume_recording();
/// # window.end_recording("output.mp4", 30).unwrap();
/// # }
/// ```
pub fn resume_recording(&mut self) {
if let Some(ref mut recording) = self.recording {
recording.paused = false;
}
}
/// Stops recording and encodes the captured frames to an MP4 video file.
///
/// This method consumes all recorded frames and encodes them using H.264 codec
/// with proper compression via FFmpeg (through the `video-rs` crate).
///
/// **Note:** This feature requires the `recording` feature to be enabled and
/// FFmpeg libraries to be installed on the system.
///
/// # Arguments
/// * `path` - The output file path for the video (should end in `.mp4`)
/// * `fps` - The frames per second for the output video
///
/// # Returns
/// * `Ok(())` on success
/// * `Err(String)` with an error message if encoding fails
///
/// # Example
/// ```no_run
/// # use kiss3d::window::Window;
/// # #[kiss3d::main]
/// # async fn main() {
/// # let mut window = Window::new("Example").await;
/// window.begin_recording();
/// for _ in 0..120 {
/// // Animate your scene...
/// window.render().await;
/// }
/// // Save as 30fps video (120 frames = 4 seconds)
/// window.end_recording("animation.mp4", 30).unwrap();
/// # }
/// ```
pub fn end_recording<P: AsRef<Path>>(&mut self, path: P, fps: u32) -> Result<(), String> {
use ffmpeg::{
codec, encoder, format, frame, software::scaling, Dictionary, Packet, Rational,
};
use ffmpeg_the_third as ffmpeg;
let recording = self
.recording
.take()
.ok_or_else(|| "No recording in progress".to_string())?;
if recording.frames.is_empty() {
return Err("No frames were recorded".to_string());
}
let width = recording.width;
let height = recording.height;
// Initialize FFmpeg (safe to call multiple times)
ffmpeg::init().map_err(|e| format!("Failed to initialize FFmpeg: {}", e))?;
// Create output context
let mut octx =
format::output(&path).map_err(|e| format!("Failed to create output context: {}", e))?;
// Check if global header is required before borrowing octx mutably
let global_header = octx.format().flags().contains(format::Flags::GLOBAL_HEADER);
// Find H.264 encoder
let codec = encoder::find(codec::Id::H264).ok_or_else(|| {
"H.264 encoder not found. Install FFmpeg with libx264 support.".to_string()
})?;
// Add video stream
let mut ost = octx
.add_stream(Some(codec))
.map_err(|e| format!("Failed to add stream: {}", e))?;
let ost_index = ost.index();
// Configure encoder
let mut encoder_ctx = codec::context::Context::new_with_codec(codec)
.encoder()
.video()
.map_err(|e| format!("Failed to create encoder context: {}", e))?;
encoder_ctx.set_width(width);
encoder_ctx.set_height(height);
encoder_ctx.set_format(format::Pixel::YUV420P);
encoder_ctx.set_time_base(Rational::new(1, fps as i32));
encoder_ctx.set_frame_rate(Some(Rational::new(fps as i32, 1)));
// Set global header flag if required by container format
if global_header {
encoder_ctx.set_flags(codec::Flags::GLOBAL_HEADER);
}
// Open encoder with x264 preset
let mut x264_opts = Dictionary::new();
x264_opts.set("preset", "medium");
x264_opts.set("crf", "23");
let mut encoder = encoder_ctx
.open_with(x264_opts)
.map_err(|e| format!("Failed to open encoder: {}", e))?;
// Set stream parameters from encoder
ost.set_parameters(codec::Parameters::from(&encoder));
// Write header
octx.write_header()
.map_err(|e| format!("Failed to write header: {}", e))?;
// Create scaler to convert RGB24 to YUV420P
let mut scaler = scaling::Context::get(
format::Pixel::RGB24,
width,
height,
format::Pixel::YUV420P,
width,
height,
scaling::Flags::BILINEAR,
)
.map_err(|e| format!("Failed to create scaler: {}", e))?;
let ost_time_base = octx.stream(ost_index).unwrap().time_base();
// Encode each frame
for (i, img_frame) in recording.frames.into_iter().enumerate() {
// Create RGB frame from captured image
let raw_data: Vec<u8> = img_frame.into_raw();
let mut rgb_frame = frame::Video::new(format::Pixel::RGB24, width, height);
rgb_frame.data_mut(0).copy_from_slice(&raw_data);
// Scale to YUV420P
let mut yuv_frame = frame::Video::empty();
scaler
.run(&rgb_frame, &mut yuv_frame)
.map_err(|e| format!("Failed to scale frame: {}", e))?;
// Set PTS (presentation timestamp)
yuv_frame.set_pts(Some(i as i64));
// Send frame to encoder
encoder
.send_frame(&yuv_frame)
.map_err(|e| format!("Failed to send frame: {}", e))?;
// Receive and write encoded packets
let mut packet = Packet::empty();
while encoder.receive_packet(&mut packet).is_ok() {
packet.set_stream(ost_index);
packet.rescale_ts(Rational::new(1, fps as i32), ost_time_base);
packet
.write_interleaved(&mut octx)
.map_err(|e| format!("Failed to write packet: {}", e))?;
}
}
// Flush encoder
encoder
.send_eof()
.map_err(|e| format!("Failed to send EOF: {}", e))?;
let mut packet = Packet::empty();
while encoder.receive_packet(&mut packet).is_ok() {
packet.set_stream(ost_index);
packet.rescale_ts(Rational::new(1, fps as i32), ost_time_base);
packet
.write_interleaved(&mut octx)
.map_err(|e| format!("Failed to write packet: {}", e))?;
}
// Write trailer
octx.write_trailer()
.map_err(|e| format!("Failed to write trailer: {}", e))?;
Ok(())
}
/// Captures the current frame if recording is active, not paused, and frame skip allows.
///
/// This is called automatically during `render()` when recording is enabled.
pub(crate) fn capture_frame_if_recording(&mut self) {
// Check if we should capture this frame
let should_capture = if let Some(ref mut recording) = self.recording {
if recording.paused {
false
} else {
recording.frame_counter += 1;
// Capture if frame_counter matches the skip interval
(recording.frame_counter - 1) % recording.config.frame_skip == 0
}
} else {
false
};
if should_capture {
let frame = self.snap_image();
let (current_width, current_height) = self.canvas.size();
// Now we can mutably borrow recording
if let Some(ref mut recording) = self.recording {
// Check if window was resized during recording
if current_width != recording.width || current_height != recording.height {
// For now, we'll just capture at current size
// A more robust solution would resize frames or fail
recording.width = current_width;
recording.height = current_height;
}
recording.frames.push(frame);
}
}
}
}