1use crate::video::error::{VideoError, VideoResult};
2use crate::video::scheduler::ScheduledFrame;
3use crossbeam_channel::{Receiver, SendTimeoutError, Sender};
4use ffmpeg_next as ffmpeg;
5use std::ffi::{CString, c_char};
6use std::path::Path;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
9use std::thread;
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum HwAccel {
14 Auto,
16 Vaapi,
17 Nvdec,
18 VideoToolbox,
19 Software,
20}
21
22impl HwAccel {
23 pub fn name(&self) -> &'static str {
24 match self {
25 HwAccel::Auto => "Auto",
26 HwAccel::Vaapi => "VAAPI",
27 HwAccel::Nvdec => "NVDEC",
28 HwAccel::VideoToolbox => "VideoToolbox",
29 HwAccel::Software => "Software",
30 }
31 }
32
33 const fn code(self) -> u8 {
34 match self {
35 HwAccel::Auto => 0,
36 HwAccel::Software => 1,
37 HwAccel::Vaapi => 2,
38 HwAccel::Nvdec => 3,
39 HwAccel::VideoToolbox => 4,
40 }
41 }
42
43 const fn from_code(code: u8) -> HwAccel {
44 match code {
45 2 => HwAccel::Vaapi,
46 3 => HwAccel::Nvdec,
47 4 => HwAccel::VideoToolbox,
48 1 => HwAccel::Software,
49 _ => HwAccel::Auto,
50 }
51 }
52
53 pub fn from_config(value: &str) -> HwAccel {
54 match value.trim().to_ascii_lowercase().as_str() {
55 "vaapi" => HwAccel::Vaapi,
56 "nvdec" | "nvidia" | "cuda" => HwAccel::Nvdec,
57 "software" | "none" | "off" => HwAccel::Software,
58 _ => HwAccel::Auto,
59 }
60 }
61
62 fn all_hardware() -> &'static [HwAccel] {
66 &[HwAccel::Nvdec, HwAccel::Vaapi, HwAccel::VideoToolbox]
67 }
68}
69
70#[derive(Debug, Clone)]
71pub struct VideoMetadata {
72 pub width: u32,
73 pub height: u32,
74 pub duration: Duration,
75 pub fps: f64,
76 pub codec: String,
77 pub format: String,
78 pub total_frames: u64,
79}
80
81#[derive(Debug, Clone)]
82pub struct VideoFrame {
83 pub data: Vec<u8>,
84 pub width: u32,
85 pub height: u32,
86 pub pts: Duration,
87 pub index: u64,
88}
89
90impl VideoFrame {
91 pub fn into_scheduled(self) -> ScheduledFrame {
92 ScheduledFrame::new(self.data, self.width, self.height, self.pts, self.index)
93 }
94}
95
96#[derive(Debug, Clone)]
97pub struct DecoderInfo {
98 pub codec_name: String,
99 pub hardware_accel: Option<String>,
100 pub pixel_format: String,
101}
102
103#[derive(Debug, Clone, Copy)]
104enum DecoderControl {
105 Pause,
106 Resume,
107 Seek(Duration),
108}
109
110pub struct VideoDecoder {
111 metadata: VideoMetadata,
112 frame_rx: Receiver<VideoFrame>,
113 control_tx: Sender<DecoderControl>,
114 stop_flag: Arc<AtomicBool>,
115 hw_in_use: Arc<AtomicU8>,
116 decode_thread: Option<thread::JoinHandle<()>>,
117}
118
119impl VideoDecoder {
120 pub fn new<P: AsRef<Path>>(path: P, hw_accel: HwAccel) -> VideoResult<Self> {
121 let path = path.as_ref().to_path_buf();
122
123 ffmpeg::init()
124 .map_err(|e| VideoError::SoftwareDecoderInit(anyhow::anyhow!("FFmpeg init: {}", e)))?;
125
126 let metadata = Self::extract_metadata(&path)?;
127
128 tracing::info!(
129 "Opened video: {}x{} @ {:.2} fps, duration: {:?}, codec: {}",
130 metadata.width,
131 metadata.height,
132 metadata.fps,
133 metadata.duration,
134 metadata.codec
135 );
136
137 let (frame_tx, frame_rx) = crossbeam_channel::bounded(3);
138 let (control_tx, control_rx) = crossbeam_channel::unbounded();
139
140 let stop_flag = Arc::new(AtomicBool::new(false));
141 let stop_flag_clone = stop_flag.clone();
142 let hw_in_use = Arc::new(AtomicU8::new(0));
143 let hw_in_use_clone = hw_in_use.clone();
144
145 let decode_thread = thread::Builder::new()
146 .name("wallr-video-decoder".to_string())
147 .spawn(move || {
148 let used = Self::decode_loop(
149 path,
150 hw_accel,
151 frame_tx,
152 control_rx,
153 stop_flag_clone,
154 hw_in_use_clone.clone(),
155 );
156 let used = match used {
157 Ok(used) => used,
158 Err(e) => {
159 tracing::error!("Video decode loop: {}", e);
160 HwAccel::Software
161 }
162 };
163 tracing::info!("Decode thread exited (backend: {})", used.name());
164 hw_in_use_clone.store(used.code(), Ordering::Relaxed);
166 })
167 .map_err(|e| VideoError::SoftwareDecoderInit(e.into()))?;
168
169 Ok(Self {
170 metadata,
171 frame_rx,
172 control_tx,
173 stop_flag,
174 hw_in_use,
175 decode_thread: Some(decode_thread),
176 })
177 }
178
179 fn extract_metadata(path: &Path) -> VideoResult<VideoMetadata> {
180 let ictx = ffmpeg::format::input(&path).map_err(|e| VideoError::FileOpen {
181 path: path.to_path_buf(),
182 source: std::io::Error::other(e.to_string()),
183 })?;
184
185 let stream = ictx
186 .streams()
187 .best(ffmpeg::media::Type::Video)
188 .ok_or_else(|| VideoError::NoVideoStream(path.to_path_buf()))?;
189
190 let decoder = ffmpeg::codec::context::Context::from_parameters(stream.parameters())
191 .and_then(|ctx| ctx.decoder().video())
192 .map_err(|e| VideoError::SoftwareDecoderInit(e.into()))?;
193
194 let width = decoder.width();
195 let height = decoder.height();
196 let codec = decoder
197 .codec()
198 .map(|c| c.name().to_string())
199 .unwrap_or_else(|| "unknown".to_string());
200
201 let frame_rate = stream.avg_frame_rate();
202 let fps = if frame_rate.numerator() > 0 {
203 frame_rate.numerator() as f64 / frame_rate.denominator() as f64
204 } else {
205 30.0
206 };
207
208 let duration = {
209 let duration_ts = stream.duration();
210 let time_base = stream.time_base();
211 if duration_ts > 0 {
212 Duration::from_secs_f64(
213 duration_ts as f64 * time_base.numerator() as f64
214 / time_base.denominator() as f64,
215 )
216 } else {
217 let container_duration = ictx.duration() as f64 / ffmpeg::ffi::AV_TIME_BASE as f64;
218 Duration::from_secs_f64(container_duration)
219 }
220 };
221
222 let total_frames = if fps > 0.0 {
223 (duration.as_secs_f64() * fps) as u64
224 } else {
225 0
226 };
227
228 Ok(VideoMetadata {
229 width,
230 height,
231 duration,
232 fps,
233 codec,
234 format: ictx.format().name().to_string(),
235 total_frames,
236 })
237 }
238
239 fn init_hw_device(hw_accel: HwAccel) -> Option<(*mut ffmpeg::ffi::AVBufferRef, &'static str)> {
240 let (type_name, device) = match hw_accel {
241 HwAccel::Vaapi => ("vaapi", Some(c"/dev/dri/renderD128")),
242 HwAccel::Nvdec => ("cuda", Some(c"0")),
243 HwAccel::VideoToolbox => ("videotoolbox", None),
244 HwAccel::Software | HwAccel::Auto => return None,
245 };
246
247 let type_name_c = CString::new(type_name).ok()?;
248
249 unsafe {
251 let hw_type = ffmpeg::ffi::av_hwdevice_find_type_by_name(type_name_c.as_ptr());
252 if hw_type == ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_NONE {
253 tracing::warn!("{} hardware type unavailable", hw_accel.name());
254 return None;
255 }
256
257 let mut device_ctx: *mut ffmpeg::ffi::AVBufferRef = std::ptr::null_mut();
258 let device_ptr: *const c_char = device.map(|d| d.as_ptr()).unwrap_or(std::ptr::null());
259 let ret = ffmpeg::ffi::av_hwdevice_ctx_create(
260 &mut device_ctx,
261 hw_type,
262 device_ptr,
263 std::ptr::null_mut(),
264 0,
265 );
266 if ret < 0 || device_ctx.is_null() {
267 tracing::warn!("{} device init failed ({})", hw_accel.name(), ret);
268 return None;
269 }
270 Some((device_ctx, type_name))
271 }
272 }
273
274 fn try_hw_decoder(
276 stream: &ffmpeg::format::stream::Stream,
277 hw_accel: HwAccel,
278 ) -> Option<(ffmpeg::codec::decoder::Video, HwAccel)> {
279 let (device_ctx, _) = Self::init_hw_device(hw_accel)?;
280
281 let mut context =
282 ffmpeg::codec::context::Context::from_parameters(stream.parameters()).ok()?;
283
284 unsafe {
287 (*context.as_mut_ptr()).hw_device_ctx = device_ctx;
288 }
289
290 match context.decoder().video() {
291 Ok(decoder) => {
292 tracing::info!("Hardware decode active: {}", hw_accel.name());
293 Some((decoder, hw_accel))
294 }
295 Err(e) => {
296 tracing::warn!("{} decode init failed: {}", hw_accel.name(), e);
297 None
298 }
299 }
300 }
301
302 fn build_decoder(
308 stream: &ffmpeg::format::stream::Stream,
309 hw_accel: HwAccel,
310 ) -> (ffmpeg::codec::decoder::Video, HwAccel) {
311 match hw_accel {
312 HwAccel::Auto => {
313 for &backend in HwAccel::all_hardware() {
315 if let Some(result) = Self::try_hw_decoder(stream, backend) {
316 return result;
317 }
318 }
319 tracing::info!("All hardware backends failed, using software decoder");
321 }
322 HwAccel::Software => {
323 tracing::info!("Software decoder explicitly requested");
325 }
326 specific => {
327 if let Some(result) = Self::try_hw_decoder(stream, specific) {
329 return result;
330 }
331 tracing::info!(
333 "{} hardware decoder failed, falling back to software",
334 specific.name()
335 );
336 }
337 }
338
339 let decoder = ffmpeg::codec::context::Context::from_parameters(stream.parameters())
341 .and_then(|ctx| ctx.decoder().video())
342 .expect("software decoder must be available");
343 (decoder, HwAccel::Software)
344 }
345
346 #[allow(clippy::too_many_arguments)]
347 fn decode_loop(
348 path: std::path::PathBuf,
349 hw_accel: HwAccel,
350 frame_tx: Sender<VideoFrame>,
351 control_rx: Receiver<DecoderControl>,
352 stop_flag: Arc<AtomicBool>,
353 hw_in_use: Arc<AtomicU8>,
354 ) -> VideoResult<HwAccel> {
355 let mut ictx = ffmpeg::format::input(&path).map_err(|e| VideoError::FileOpen {
356 path: path.clone(),
357 source: std::io::Error::other(e.to_string()),
358 })?;
359
360 let stream = ictx
361 .streams()
362 .best(ffmpeg::media::Type::Video)
363 .ok_or_else(|| VideoError::NoVideoStream(path.clone()))?;
364
365 let video_stream_index = stream.index();
366 let time_base = stream.time_base();
367
368 let (mut decoder, used_hw) = Self::build_decoder(&stream, hw_accel);
369 tracing::info!("Decoder in use: {}", used_hw.name());
370
371 hw_in_use.store(used_hw.code(), Ordering::Relaxed);
373
374 let mut scaler: Option<ffmpeg::software::scaling::Context> = None;
375 let mut scaler_src: Option<ffmpeg::format::Pixel> = None;
376
377 let mut paused = false;
378 let mut pending_seek: Option<Duration> = None;
379 let mut frame_index = 0u64;
380 let mut decoded_frame = ffmpeg::frame::Video::empty();
381 let mut sw_frame = ffmpeg::frame::Video::empty();
382 let mut rgb_frame = ffmpeg::frame::Video::empty();
383
384 'outer: loop {
385 if stop_flag.load(Ordering::Relaxed) {
386 break;
387 }
388
389 while let Ok(control) = control_rx.try_recv() {
390 match control {
391 DecoderControl::Pause => paused = true,
392 DecoderControl::Resume => paused = false,
393 DecoderControl::Seek(ts) => pending_seek = Some(ts),
394 }
395 }
396
397 if let Some(ts) = pending_seek.take() {
398 Self::apply_seek(&mut ictx, &mut decoder, time_base, ts);
399 }
400
401 if paused {
402 thread::sleep(Duration::from_millis(10));
403 continue;
404 }
405
406 for (stream, packet) in ictx.packets() {
407 if stop_flag.load(Ordering::Relaxed) {
408 break 'outer;
409 }
410
411 while let Ok(control) = control_rx.try_recv() {
412 match control {
413 DecoderControl::Pause => paused = true,
414 DecoderControl::Resume => paused = false,
415 DecoderControl::Seek(ts) => pending_seek = Some(ts),
416 }
417 }
418 if paused || pending_seek.is_some() {
419 break;
420 }
421
422 if stream.index() != video_stream_index {
423 continue;
424 }
425
426 decoder
427 .send_packet(&packet)
428 .map_err(|e| VideoError::DecodeFailed(anyhow::anyhow!("send_packet: {}", e)))?;
429
430 while decoder.receive_frame(&mut decoded_frame).is_ok() {
431 if stop_flag.load(Ordering::Relaxed) {
432 break 'outer;
433 }
434
435 let is_hw_frame = unsafe { !(*decoded_frame.as_ptr()).hw_frames_ctx.is_null() };
436
437 let src_frame = if is_hw_frame {
438 let ret = unsafe {
439 ffmpeg::ffi::av_frame_unref(sw_frame.as_mut_ptr());
440 ffmpeg::ffi::av_hwframe_transfer_data(
441 sw_frame.as_mut_ptr(),
442 decoded_frame.as_ptr(),
443 0,
444 )
445 };
446 if ret < 0 {
447 tracing::warn!("hwframe transfer failed: {ret}");
448 continue;
449 }
450 &sw_frame
451 } else {
452 &decoded_frame
453 };
454
455 let src_format = src_frame.format();
456 if scaler_src != Some(src_format) {
457 scaler = Some(
458 ffmpeg::software::scaling::context::Context::get(
459 src_format,
460 src_frame.width(),
461 src_frame.height(),
462 ffmpeg::format::Pixel::RGBA,
463 src_frame.width(),
464 src_frame.height(),
465 ffmpeg::software::scaling::Flags::BILINEAR,
466 )
467 .map_err(|e| VideoError::FormatConversionFailed(e.into()))?,
468 );
469 scaler_src = Some(src_format);
470 }
471
472 scaler
473 .as_mut()
474 .expect("scaler initialized above")
475 .run(src_frame, &mut rgb_frame)
476 .map_err(|e| VideoError::FormatConversionFailed(e.into()))?;
477
478 let pts_duration = if let Some(pts) = decoded_frame.timestamp() {
479 Duration::from_secs_f64(
480 pts as f64 * time_base.numerator() as f64
481 / time_base.denominator() as f64,
482 )
483 } else {
484 Duration::from_secs_f64(frame_index as f64 / 30.0)
485 };
486
487 let video_frame = VideoFrame {
488 data: rgb_frame.data(0).to_vec(),
489 width: rgb_frame.width(),
490 height: rgb_frame.height(),
491 pts: pts_duration,
492 index: frame_index,
493 };
494 frame_index = frame_index.wrapping_add(1);
495
496 match frame_tx.send_timeout(video_frame, Duration::from_millis(20)) {
497 Ok(()) => {}
498 Err(SendTimeoutError::Timeout(_)) => break,
499 Err(SendTimeoutError::Disconnected(_)) => {
500 tracing::warn!("Frame queue disconnected, ending decode loop");
501 return Ok(used_hw);
502 }
503 }
504 }
505 }
506
507 tracing::debug!("End of stream after {} frames, looping", frame_index);
508 while decoder.receive_frame(&mut decoded_frame).is_ok() {
509 if stop_flag.load(Ordering::Relaxed) {
510 break 'outer;
511 }
512 }
513 if stop_flag.load(Ordering::Relaxed) {
514 break;
515 }
516 if !paused {
517 ictx.seek(0, ..)
518 .map_err(|e| VideoError::SeekFailed(Duration::ZERO, e.into()))?;
519 decoder.flush();
520 }
521 }
522
523 Ok(used_hw)
524 }
525
526 fn apply_seek(
527 ictx: &mut ffmpeg::format::context::Input,
528 decoder: &mut ffmpeg::codec::decoder::Video,
529 time_base: ffmpeg::Rational,
530 ts: Duration,
531 ) {
532 let tb_sec = time_base.numerator() as f64 / time_base.denominator() as f64;
533 let ts_tb = if tb_sec > 0.0 {
534 (ts.as_secs_f64() / tb_sec) as i64
535 } else {
536 0
537 };
538 match ictx.seek(ts_tb, ..) {
539 Ok(()) => {
540 decoder.flush();
541 let mut drain = ffmpeg::frame::Video::empty();
542 while decoder.receive_frame(&mut drain).is_ok() {}
543 tracing::info!("Seeked to {:?}", ts);
544 }
545 Err(e) => tracing::warn!("Seek to {:?} failed: {}", ts, e),
546 }
547 }
548
549 pub fn next_frame(&self) -> Option<VideoFrame> {
550 self.frame_rx.try_recv().ok()
551 }
552
553 pub fn metadata(&self) -> &VideoMetadata {
554 &self.metadata
555 }
556
557 pub fn decoder_info(&self) -> DecoderInfo {
558 DecoderInfo {
559 codec_name: self.metadata.codec.clone(),
560 hardware_accel: if self.hw_accel_in_use() != HwAccel::Software {
561 Some(self.hw_accel_in_use().name().to_string())
562 } else {
563 None
564 },
565 pixel_format: "RGBA".to_string(),
566 }
567 }
568
569 pub fn hw_accel_in_use(&self) -> HwAccel {
570 for _ in 0..50 {
571 let code = self.hw_in_use.load(Ordering::Relaxed);
572 if code != 0 {
573 return HwAccel::from_code(code);
574 }
575 std::thread::sleep(Duration::from_millis(5));
576 }
577 HwAccel::Software
578 }
579
580 pub fn pause(&self) {
581 let _ = self.control_tx.send(DecoderControl::Pause);
582 }
583
584 pub fn resume(&self) {
585 let _ = self.control_tx.send(DecoderControl::Resume);
586 }
587
588 pub fn seek(&self, timestamp: Duration) {
589 let _ = self.control_tx.send(DecoderControl::Seek(timestamp));
590 }
591
592 pub fn drain(&mut self) {
593 while self.frame_rx.try_recv().is_ok() {}
594 }
595
596 pub fn is_video_file<P: AsRef<Path>>(path: P) -> bool {
597 path.as_ref()
598 .extension()
599 .and_then(|e| e.to_str())
600 .is_some_and(|ext| {
601 matches!(
602 ext.to_lowercase().as_str(),
603 "mp4" | "webm" | "mkv" | "mov" | "avi" | "m4v"
604 )
605 })
606 }
607}
608
609impl Drop for VideoDecoder {
610 fn drop(&mut self) {
611 self.stop_flag.store(true, Ordering::Relaxed);
612 if let Some(thread) = self.decode_thread.take() {
613 let _ = thread.join();
614 }
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn test_is_video_file() {
624 assert!(VideoDecoder::is_video_file("test.mp4"));
625 assert!(VideoDecoder::is_video_file("test.MP4"));
626 assert!(VideoDecoder::is_video_file("test.webm"));
627 assert!(VideoDecoder::is_video_file("test.mkv"));
628 assert!(VideoDecoder::is_video_file("test.mov"));
629 assert!(!VideoDecoder::is_video_file("test.jpg"));
630 assert!(!VideoDecoder::is_video_file("test.gif"));
631 assert!(!VideoDecoder::is_video_file("test.png"));
632 }
633
634 #[test]
635 fn test_hwaccel_names() {
636 assert_eq!(HwAccel::Vaapi.name(), "VAAPI");
637 assert_eq!(HwAccel::Nvdec.name(), "NVDEC");
638 assert_eq!(HwAccel::Software.name(), "Software");
639 }
640
641 #[test]
642 fn test_hwaccel_codes_roundtrip() {
643 for accel in [
644 HwAccel::Software,
645 HwAccel::Vaapi,
646 HwAccel::Nvdec,
647 HwAccel::VideoToolbox,
648 ] {
649 assert_eq!(HwAccel::from_code(accel.code()), accel);
650 }
651 }
652
653 #[test]
654 fn test_hwaccel_from_config() {
655 assert_eq!(HwAccel::from_config("vaapi"), HwAccel::Vaapi);
656 assert_eq!(HwAccel::from_config("nvdec"), HwAccel::Nvdec);
657 assert_eq!(HwAccel::from_config("software"), HwAccel::Software);
658 assert_eq!(HwAccel::from_config("auto"), HwAccel::Auto);
659 assert_eq!(HwAccel::from_config("unknown"), HwAccel::Auto);
660 }
661}