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