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, AtomicU64, 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 enum VideoFrameData {
83 Rgba(Vec<u8>),
84 Nv12 {
85 y_plane: Vec<u8>,
86 uv_plane: Vec<u8>,
87 color: YuvColorInfo,
88 },
89}
90
91impl VideoFrameData {
92 pub fn len(&self) -> usize {
93 match self {
94 Self::Rgba(data) => data.len(),
95 Self::Nv12 {
96 y_plane, uv_plane, ..
97 } => y_plane.len() + uv_plane.len(),
98 }
99 }
100
101 pub fn is_empty(&self) -> bool {
102 self.len() == 0
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct YuvColorInfo {
108 pub matrix: YuvMatrix,
109 pub range: YuvRange,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum YuvMatrix {
114 Bt601,
115 Bt709,
116 Bt2020,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum YuvRange {
121 Limited,
122 Full,
123}
124
125#[derive(Debug, Clone)]
126pub struct VideoFrame {
127 pub data: VideoFrameData,
128 pub width: u32,
129 pub height: u32,
130 pub pts: Duration,
131 pub index: u64,
132}
133
134impl VideoFrame {
135 pub fn into_scheduled(self) -> ScheduledFrame {
136 ScheduledFrame::new(self.data, self.width, self.height, self.pts, self.index)
137 }
138}
139
140#[derive(Debug, Clone)]
141pub struct DecoderInfo {
142 pub codec_name: String,
143 pub hardware_accel: Option<String>,
144 pub pixel_format: String,
145}
146
147#[derive(Debug, Clone, Copy)]
148enum DecoderControl {
149 Pause,
150 Resume,
151 Seek(Duration, u64),
152}
153
154pub struct VideoDecoder {
155 metadata: VideoMetadata,
156 frame_rx: Receiver<VideoFrame>,
157 control_tx: Sender<DecoderControl>,
158 stop_flag: Arc<AtomicBool>,
159 seek_epoch: Arc<AtomicU64>,
160 hw_in_use: Arc<AtomicU8>,
161 decode_thread: Option<thread::JoinHandle<()>>,
162}
163
164impl VideoDecoder {
165 pub fn new<P: AsRef<Path>>(path: P, hw_accel: HwAccel) -> VideoResult<Self> {
166 Self::with_preload(path, hw_accel, 2)
167 }
168
169 pub fn with_preload<P: AsRef<Path>>(
170 path: P,
171 hw_accel: HwAccel,
172 preload_frames: usize,
173 ) -> VideoResult<Self> {
174 let path = path.as_ref().to_path_buf();
175
176 ffmpeg::init()
177 .map_err(|e| VideoError::SoftwareDecoderInit(anyhow::anyhow!("FFmpeg init: {}", e)))?;
178
179 let metadata = Self::extract_metadata(&path)?;
180
181 tracing::info!(
182 "Opened video: {}x{} @ {:.2} fps, duration: {:?}, codec: {}",
183 metadata.width,
184 metadata.height,
185 metadata.fps,
186 metadata.duration,
187 metadata.codec
188 );
189
190 let (frame_tx, frame_rx) = crossbeam_channel::bounded(preload_frames.max(1));
191 let (control_tx, control_rx) = crossbeam_channel::unbounded();
192
193 let stop_flag = Arc::new(AtomicBool::new(false));
194 let stop_flag_clone = stop_flag.clone();
195 let seek_epoch = Arc::new(AtomicU64::new(0));
196 let seek_epoch_clone = seek_epoch.clone();
197 let hw_in_use = Arc::new(AtomicU8::new(0));
198 let hw_in_use_clone = hw_in_use.clone();
199
200 let decode_thread = thread::Builder::new()
201 .name("wallr-video-decoder".to_string())
202 .spawn(move || {
203 let used = Self::decode_loop(
204 path,
205 hw_accel,
206 frame_tx,
207 control_rx,
208 stop_flag_clone,
209 seek_epoch_clone,
210 hw_in_use_clone.clone(),
211 );
212 let used = match used {
213 Ok(used) => used,
214 Err(e) => {
215 tracing::error!("Video decode loop: {}", e);
216 HwAccel::Software
217 }
218 };
219 tracing::info!("Decode thread exited (backend: {})", used.name());
220 hw_in_use_clone.store(used.code(), Ordering::Relaxed);
222 })
223 .map_err(|e| VideoError::SoftwareDecoderInit(e.into()))?;
224
225 Ok(Self {
226 metadata,
227 frame_rx,
228 control_tx,
229 stop_flag,
230 seek_epoch,
231 hw_in_use,
232 decode_thread: Some(decode_thread),
233 })
234 }
235
236 fn extract_metadata(path: &Path) -> VideoResult<VideoMetadata> {
237 let ictx = ffmpeg::format::input(&path).map_err(|e| VideoError::FileOpen {
238 path: path.to_path_buf(),
239 source: std::io::Error::other(e.to_string()),
240 })?;
241
242 let stream = ictx
243 .streams()
244 .best(ffmpeg::media::Type::Video)
245 .ok_or_else(|| VideoError::NoVideoStream(path.to_path_buf()))?;
246
247 let decoder = ffmpeg::codec::context::Context::from_parameters(stream.parameters())
248 .and_then(|ctx| ctx.decoder().video())
249 .map_err(|e| VideoError::SoftwareDecoderInit(e.into()))?;
250
251 let width = decoder.width();
252 let height = decoder.height();
253 let codec = decoder
254 .codec()
255 .map(|c| c.name().to_string())
256 .unwrap_or_else(|| "unknown".to_string());
257
258 let frame_rate = stream.avg_frame_rate();
259 let fps = if frame_rate.numerator() > 0 {
260 frame_rate.numerator() as f64 / frame_rate.denominator() as f64
261 } else {
262 30.0
263 };
264
265 let duration = {
266 let duration_ts = stream.duration();
267 let time_base = stream.time_base();
268 if duration_ts > 0 {
269 Duration::from_secs_f64(
270 duration_ts as f64 * time_base.numerator() as f64
271 / time_base.denominator() as f64,
272 )
273 } else {
274 let container_duration = ictx.duration() as f64 / ffmpeg::ffi::AV_TIME_BASE as f64;
275 Duration::from_secs_f64(container_duration)
276 }
277 };
278
279 let total_frames = if fps > 0.0 {
280 (duration.as_secs_f64() * fps) as u64
281 } else {
282 0
283 };
284
285 Ok(VideoMetadata {
286 width,
287 height,
288 duration,
289 fps,
290 codec,
291 format: ictx.format().name().to_string(),
292 total_frames,
293 })
294 }
295
296 fn init_hw_device(hw_accel: HwAccel) -> Option<(*mut ffmpeg::ffi::AVBufferRef, &'static str)> {
297 let (type_name, device) = match hw_accel {
298 HwAccel::Vaapi => ("vaapi", Some(c"/dev/dri/renderD128")),
299 HwAccel::Nvdec => ("cuda", Some(c"0")),
300 HwAccel::VideoToolbox => ("videotoolbox", None),
301 HwAccel::Software | HwAccel::Auto => return None,
302 };
303
304 let type_name_c = CString::new(type_name).ok()?;
305
306 unsafe {
308 let hw_type = ffmpeg::ffi::av_hwdevice_find_type_by_name(type_name_c.as_ptr());
309 if hw_type == ffmpeg::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_NONE {
310 tracing::warn!("{} hardware type unavailable", hw_accel.name());
311 return None;
312 }
313
314 let mut device_ctx: *mut ffmpeg::ffi::AVBufferRef = std::ptr::null_mut();
315 let device_ptr: *const c_char = device.map(|d| d.as_ptr()).unwrap_or(std::ptr::null());
316 let ret = ffmpeg::ffi::av_hwdevice_ctx_create(
317 &mut device_ctx,
318 hw_type,
319 device_ptr,
320 std::ptr::null_mut(),
321 0,
322 );
323 if ret < 0 || device_ctx.is_null() {
324 tracing::warn!("{} device init failed ({})", hw_accel.name(), ret);
325 return None;
326 }
327 Some((device_ctx, type_name))
328 }
329 }
330
331 fn try_hw_decoder(
333 stream: &ffmpeg::format::stream::Stream,
334 hw_accel: HwAccel,
335 ) -> Option<(ffmpeg::codec::decoder::Video, HwAccel)> {
336 let (device_ctx, _) = Self::init_hw_device(hw_accel)?;
337
338 let mut context =
339 ffmpeg::codec::context::Context::from_parameters(stream.parameters()).ok()?;
340
341 unsafe {
344 (*context.as_mut_ptr()).hw_device_ctx = device_ctx;
345 }
346
347 match context.decoder().video() {
348 Ok(decoder) => {
349 tracing::info!("Hardware decode active: {}", hw_accel.name());
350 Some((decoder, hw_accel))
351 }
352 Err(e) => {
353 tracing::warn!("{} decode init failed: {}", hw_accel.name(), e);
354 None
355 }
356 }
357 }
358
359 fn build_decoder(
365 stream: &ffmpeg::format::stream::Stream,
366 hw_accel: HwAccel,
367 ) -> (ffmpeg::codec::decoder::Video, HwAccel) {
368 match hw_accel {
369 HwAccel::Auto => {
370 for &backend in HwAccel::all_hardware() {
372 if let Some(result) = Self::try_hw_decoder(stream, backend) {
373 return result;
374 }
375 }
376 tracing::info!("All hardware backends failed, using software decoder");
378 }
379 HwAccel::Software => {
380 tracing::info!("Software decoder explicitly requested");
382 }
383 specific => {
384 if let Some(result) = Self::try_hw_decoder(stream, specific) {
386 return result;
387 }
388 tracing::info!(
390 "{} hardware decoder failed, falling back to software",
391 specific.name()
392 );
393 }
394 }
395
396 let decoder = ffmpeg::codec::context::Context::from_parameters(stream.parameters())
398 .and_then(|ctx| ctx.decoder().video())
399 .expect("software decoder must be available");
400 (decoder, HwAccel::Software)
401 }
402
403 #[allow(clippy::too_many_arguments)]
404 fn decode_loop(
405 path: std::path::PathBuf,
406 hw_accel: HwAccel,
407 frame_tx: Sender<VideoFrame>,
408 control_rx: Receiver<DecoderControl>,
409 stop_flag: Arc<AtomicBool>,
410 seek_epoch: Arc<AtomicU64>,
411 hw_in_use: Arc<AtomicU8>,
412 ) -> VideoResult<HwAccel> {
413 let mut ictx = ffmpeg::format::input(&path).map_err(|e| VideoError::FileOpen {
414 path: path.clone(),
415 source: std::io::Error::other(e.to_string()),
416 })?;
417
418 let stream = ictx
419 .streams()
420 .best(ffmpeg::media::Type::Video)
421 .ok_or_else(|| VideoError::NoVideoStream(path.clone()))?;
422
423 let video_stream_index = stream.index();
424 let time_base = stream.time_base();
425
426 let (mut decoder, used_hw) = Self::build_decoder(&stream, hw_accel);
427 tracing::info!("Decoder in use: {}", used_hw.name());
428
429 hw_in_use.store(used_hw.code(), Ordering::Relaxed);
431
432 let mut scaler: Option<ffmpeg::software::scaling::Context> = None;
433 let mut scaler_src: Option<ffmpeg::format::Pixel> = None;
434
435 let mut paused = false;
436 let mut pending_seek: Option<(Duration, u64)> = None;
437 let mut applied_seek_epoch = 0;
438 let mut frame_index = 0u64;
439 let mut decoded_frame = ffmpeg::frame::Video::empty();
440 let mut sw_frame = ffmpeg::frame::Video::empty();
441 let mut rgb_frame = ffmpeg::frame::Video::empty();
442
443 'outer: loop {
444 if stop_flag.load(Ordering::Relaxed) {
445 break;
446 }
447
448 while let Ok(control) = control_rx.try_recv() {
449 match control {
450 DecoderControl::Pause => paused = true,
451 DecoderControl::Resume => paused = false,
452 DecoderControl::Seek(ts, epoch) => pending_seek = Some((ts, epoch)),
453 }
454 }
455
456 if let Some((ts, epoch)) = pending_seek.take() {
457 Self::apply_seek(&mut ictx, &mut decoder, time_base, ts);
458 applied_seek_epoch = epoch;
459 }
460
461 if paused {
462 thread::sleep(Duration::from_millis(10));
463 continue;
464 }
465
466 for (stream, packet) in ictx.packets() {
467 if stop_flag.load(Ordering::Relaxed) {
468 break 'outer;
469 }
470
471 while let Ok(control) = control_rx.try_recv() {
472 match control {
473 DecoderControl::Pause => paused = true,
474 DecoderControl::Resume => paused = false,
475 DecoderControl::Seek(ts, epoch) => pending_seek = Some((ts, epoch)),
476 }
477 }
478 if paused || pending_seek.is_some() {
479 break;
480 }
481
482 if stream.index() != video_stream_index {
483 continue;
484 }
485
486 decoder
487 .send_packet(&packet)
488 .map_err(|e| VideoError::DecodeFailed(anyhow::anyhow!("send_packet: {}", e)))?;
489
490 while decoder.receive_frame(&mut decoded_frame).is_ok() {
491 if stop_flag.load(Ordering::Relaxed) {
492 break 'outer;
493 }
494
495 let mut interrupted = false;
498 loop {
499 if stop_flag.load(Ordering::Relaxed) {
500 break 'outer;
501 }
502 while let Ok(control) = control_rx.try_recv() {
503 match control {
504 DecoderControl::Pause => paused = true,
505 DecoderControl::Resume => paused = false,
506 DecoderControl::Seek(ts, epoch) => {
507 pending_seek = Some((ts, epoch));
508 }
509 }
510 }
511 if pending_seek.is_some()
512 || seek_epoch.load(Ordering::Acquire) != applied_seek_epoch
513 {
514 interrupted = true;
515 break;
516 }
517 if !paused && !frame_tx.is_full() {
518 break;
519 }
520 thread::sleep(Duration::from_millis(1));
521 }
522 if interrupted {
523 break;
524 }
525
526 let is_hw_frame = unsafe { !(*decoded_frame.as_ptr()).hw_frames_ctx.is_null() };
527
528 let src_frame = if is_hw_frame {
529 let ret = unsafe {
530 ffmpeg::ffi::av_frame_unref(sw_frame.as_mut_ptr());
531 ffmpeg::ffi::av_hwframe_transfer_data(
532 sw_frame.as_mut_ptr(),
533 decoded_frame.as_ptr(),
534 0,
535 )
536 };
537 if ret < 0 {
538 tracing::warn!("hwframe transfer failed: {ret}");
539 continue;
540 }
541 &sw_frame
542 } else {
543 &decoded_frame
544 };
545
546 let pts_duration = if let Some(pts) = decoded_frame.timestamp() {
547 Duration::from_secs_f64(
548 pts as f64 * time_base.numerator() as f64
549 / time_base.denominator() as f64,
550 )
551 } else {
552 Duration::from_secs_f64(frame_index as f64 / 30.0)
553 };
554
555 let width = src_frame.width();
556 let height = src_frame.height();
557 let data = if src_frame.format() == ffmpeg::format::Pixel::NV12 {
558 let color_space = match decoded_frame.color_space() {
559 ffmpeg::color::Space::Unspecified => decoder.color_space(),
560 value => value,
561 };
562 let color_range = match decoded_frame.color_range() {
563 ffmpeg::color::Range::Unspecified => decoder.color_range(),
564 value => value,
565 };
566 let color = select_yuv_color(color_space, color_range, width, height);
567 let (y_plane, uv_plane) = copy_nv12_planes(src_frame);
568 VideoFrameData::Nv12 {
569 y_plane,
570 uv_plane,
571 color,
572 }
573 } else {
574 let src_format = src_frame.format();
575 if scaler_src != Some(src_format) {
576 scaler = Some(
577 ffmpeg::software::scaling::context::Context::get(
578 src_format,
579 width,
580 height,
581 ffmpeg::format::Pixel::RGBA,
582 width,
583 height,
584 ffmpeg::software::scaling::Flags::BILINEAR,
585 )
586 .map_err(|e| VideoError::FormatConversionFailed(e.into()))?,
587 );
588 scaler_src = Some(src_format);
589 }
590
591 scaler
592 .as_mut()
593 .expect("scaler initialized above")
594 .run(src_frame, &mut rgb_frame)
595 .map_err(|e| VideoError::FormatConversionFailed(e.into()))?;
596 VideoFrameData::Rgba(copy_packed_rows(
597 rgb_frame.data(0),
598 rgb_frame.stride(0),
599 rgb_frame.width() as usize * 4,
600 rgb_frame.height() as usize,
601 ))
602 };
603
604 let video_frame = VideoFrame {
605 data,
606 width,
607 height,
608 pts: pts_duration,
609 index: frame_index,
610 };
611 frame_index = frame_index.wrapping_add(1);
612
613 match frame_tx.send_timeout(video_frame, Duration::from_millis(20)) {
614 Ok(()) => {}
615 Err(SendTimeoutError::Timeout(_)) => break,
616 Err(SendTimeoutError::Disconnected(_)) => {
617 tracing::warn!("Frame queue disconnected, ending decode loop");
618 return Ok(used_hw);
619 }
620 }
621 }
622 }
623
624 tracing::debug!("End of stream after {} frames, looping", frame_index);
625 while decoder.receive_frame(&mut decoded_frame).is_ok() {
626 if stop_flag.load(Ordering::Relaxed) {
627 break 'outer;
628 }
629 }
630 if stop_flag.load(Ordering::Relaxed) {
631 break;
632 }
633 if !paused {
634 ictx.seek(0, ..)
635 .map_err(|e| VideoError::SeekFailed(Duration::ZERO, e.into()))?;
636 decoder.flush();
637 }
638 }
639
640 Ok(used_hw)
641 }
642
643 fn apply_seek(
644 ictx: &mut ffmpeg::format::context::Input,
645 decoder: &mut ffmpeg::codec::decoder::Video,
646 time_base: ffmpeg::Rational,
647 ts: Duration,
648 ) {
649 let tb_sec = time_base.numerator() as f64 / time_base.denominator() as f64;
650 let ts_tb = if tb_sec > 0.0 {
651 (ts.as_secs_f64() / tb_sec) as i64
652 } else {
653 0
654 };
655 match ictx.seek(ts_tb, ..) {
656 Ok(()) => {
657 decoder.flush();
658 let mut drain = ffmpeg::frame::Video::empty();
659 while decoder.receive_frame(&mut drain).is_ok() {}
660 tracing::info!("Seeked to {:?}", ts);
661 }
662 Err(e) => tracing::warn!("Seek to {:?} failed: {}", ts, e),
663 }
664 }
665
666 pub fn next_frame(&self) -> Option<VideoFrame> {
667 self.frame_rx.try_recv().ok()
668 }
669
670 pub fn metadata(&self) -> &VideoMetadata {
671 &self.metadata
672 }
673
674 pub fn decoder_info(&self) -> DecoderInfo {
675 DecoderInfo {
676 codec_name: self.metadata.codec.clone(),
677 hardware_accel: if self.hw_accel_in_use() != HwAccel::Software {
678 Some(self.hw_accel_in_use().name().to_string())
679 } else {
680 None
681 },
682 pixel_format: "NV12/RGBA".to_string(),
683 }
684 }
685
686 pub fn hw_accel_in_use(&self) -> HwAccel {
687 for _ in 0..50 {
688 let code = self.hw_in_use.load(Ordering::Relaxed);
689 if code != 0 {
690 return HwAccel::from_code(code);
691 }
692 std::thread::sleep(Duration::from_millis(5));
693 }
694 HwAccel::Software
695 }
696
697 pub fn pause(&self) {
698 let _ = self.control_tx.send(DecoderControl::Pause);
699 }
700
701 pub fn resume(&self) {
702 let _ = self.control_tx.send(DecoderControl::Resume);
703 }
704
705 pub fn seek(&self, timestamp: Duration) {
706 let epoch = self.seek_epoch.fetch_add(1, Ordering::AcqRel) + 1;
707 let _ = self.control_tx.send(DecoderControl::Seek(timestamp, epoch));
708 }
709
710 pub fn drain(&mut self) {
711 while self.frame_rx.try_recv().is_ok() {}
712 }
713
714 pub fn is_video_file<P: AsRef<Path>>(path: P) -> bool {
715 path.as_ref()
716 .extension()
717 .and_then(|e| e.to_str())
718 .is_some_and(|ext| {
719 matches!(
720 ext.to_lowercase().as_str(),
721 "mp4" | "webm" | "mkv" | "mov" | "avi" | "m4v"
722 )
723 })
724 }
725}
726
727fn copy_packed_rows(source: &[u8], stride: usize, row_bytes: usize, height: usize) -> Vec<u8> {
728 let mut packed = Vec::with_capacity(row_bytes * height);
729 for row in source.chunks(stride).take(height) {
730 packed.extend_from_slice(&row[..row_bytes]);
731 }
732 packed
733}
734
735fn copy_nv12_planes(frame: &ffmpeg::frame::Video) -> (Vec<u8>, Vec<u8>) {
736 copy_nv12_data(
737 frame.data(0),
738 frame.stride(0),
739 frame.data(1),
740 frame.stride(1),
741 frame.width() as usize,
742 frame.height() as usize,
743 )
744}
745
746fn copy_nv12_data(
747 y_source: &[u8],
748 y_stride: usize,
749 uv_source: &[u8],
750 uv_stride: usize,
751 width: usize,
752 height: usize,
753) -> (Vec<u8>, Vec<u8>) {
754 let chroma_width = width.div_ceil(2);
755 let chroma_height = height.div_ceil(2);
756 (
757 copy_packed_rows(y_source, y_stride, width, height),
758 copy_packed_rows(uv_source, uv_stride, chroma_width * 2, chroma_height),
759 )
760}
761
762fn select_yuv_color(
763 space: ffmpeg::color::Space,
764 range: ffmpeg::color::Range,
765 width: u32,
766 height: u32,
767) -> YuvColorInfo {
768 let matrix = match space {
769 ffmpeg::color::Space::BT470BG | ffmpeg::color::Space::SMPTE170M => YuvMatrix::Bt601,
770 ffmpeg::color::Space::BT709 => YuvMatrix::Bt709,
771 ffmpeg::color::Space::BT2020NCL => YuvMatrix::Bt2020,
772 _ if width >= 1280 || height > 576 => YuvMatrix::Bt709,
773 _ => YuvMatrix::Bt601,
774 };
775 let range = match range {
776 ffmpeg::color::Range::JPEG => YuvRange::Full,
777 ffmpeg::color::Range::MPEG | ffmpeg::color::Range::Unspecified => YuvRange::Limited,
778 };
779 YuvColorInfo { matrix, range }
780}
781
782impl Drop for VideoDecoder {
783 fn drop(&mut self) {
784 self.stop_flag.store(true, Ordering::Relaxed);
785 if let Some(thread) = self.decode_thread.take() {
786 let _ = thread.join();
787 }
788 }
789}
790
791#[cfg(test)]
792mod tests {
793 use super::*;
794
795 #[test]
796 fn test_is_video_file() {
797 assert!(VideoDecoder::is_video_file("test.mp4"));
798 assert!(VideoDecoder::is_video_file("test.MP4"));
799 assert!(VideoDecoder::is_video_file("test.webm"));
800 assert!(VideoDecoder::is_video_file("test.mkv"));
801 assert!(VideoDecoder::is_video_file("test.mov"));
802 assert!(!VideoDecoder::is_video_file("test.jpg"));
803 assert!(!VideoDecoder::is_video_file("test.gif"));
804 assert!(!VideoDecoder::is_video_file("test.png"));
805 }
806
807 #[test]
808 fn test_hwaccel_names() {
809 assert_eq!(HwAccel::Vaapi.name(), "VAAPI");
810 assert_eq!(HwAccel::Nvdec.name(), "NVDEC");
811 assert_eq!(HwAccel::Software.name(), "Software");
812 }
813
814 #[test]
815 fn test_hwaccel_codes_roundtrip() {
816 for accel in [
817 HwAccel::Software,
818 HwAccel::Vaapi,
819 HwAccel::Nvdec,
820 HwAccel::VideoToolbox,
821 ] {
822 assert_eq!(HwAccel::from_code(accel.code()), accel);
823 }
824 }
825
826 #[test]
827 fn test_hwaccel_from_config() {
828 assert_eq!(HwAccel::from_config("vaapi"), HwAccel::Vaapi);
829 assert_eq!(HwAccel::from_config("nvdec"), HwAccel::Nvdec);
830 assert_eq!(HwAccel::from_config("software"), HwAccel::Software);
831 assert_eq!(HwAccel::from_config("auto"), HwAccel::Auto);
832 assert_eq!(HwAccel::from_config("unknown"), HwAccel::Auto);
833 }
834
835 #[test]
836 fn packs_strided_rows() {
837 let y = [1, 2, 3, 99, 99, 4, 5, 6, 99, 99, 7, 8, 9, 99, 99];
838 let uv = [10, 11, 12, 13, 99, 99, 14, 15, 16, 17, 99, 99];
839 let (packed_y, packed_uv) = copy_nv12_data(&y, 5, &uv, 6, 3, 3);
840 assert_eq!(packed_y, [1, 2, 3, 4, 5, 6, 7, 8, 9]);
841 assert_eq!(packed_uv, [10, 11, 12, 13, 14, 15, 16, 17]);
842 }
843
844 #[test]
845 fn selects_yuv_matrix_and_range() {
846 assert_eq!(
847 select_yuv_color(
848 ffmpeg::color::Space::BT2020NCL,
849 ffmpeg::color::Range::JPEG,
850 3840,
851 2160,
852 ),
853 YuvColorInfo {
854 matrix: YuvMatrix::Bt2020,
855 range: YuvRange::Full,
856 }
857 );
858 assert_eq!(
859 select_yuv_color(
860 ffmpeg::color::Space::Unspecified,
861 ffmpeg::color::Range::Unspecified,
862 1920,
863 1080,
864 ),
865 YuvColorInfo {
866 matrix: YuvMatrix::Bt709,
867 range: YuvRange::Limited,
868 }
869 );
870 assert_eq!(
871 select_yuv_color(
872 ffmpeg::color::Space::Unspecified,
873 ffmpeg::color::Range::MPEG,
874 720,
875 576,
876 ),
877 YuvColorInfo {
878 matrix: YuvMatrix::Bt601,
879 range: YuvRange::Limited,
880 }
881 );
882 }
883}