1use super::{AudioSource, Frame, VideoSource, CHANNELS, SAMPLE_RATE};
15use crate::model::{Asset, AudioStreamInfo, ClipKind};
16use std::sync::OnceLock;
17use windows::core::{Interface, BOOL, GUID, HSTRING};
18use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
19use windows::Win32::Graphics::Direct3D11::{
20 D3D11CreateDevice, ID3D11Device, ID3D11Multithread, D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_SDK_VERSION,
21};
22use windows::Win32::Media::MediaFoundation::*;
23use windows::Win32::System::Com::StructuredStorage::PROPVARIANT;
24use windows::Win32::System::Com::{CoInitializeEx, COINIT_MULTITHREADED};
25
26const HNS: f64 = 1e7;
28const GUID_NULL: GUID = GUID::zeroed();
29const VIDEO_FWD_HNS: i64 = 15_000_000;
31const AUDIO_FWD_FRAMES: i64 = SAMPLE_RATE as i64 / 2;
33const AUDIO_PREROLL_FRAMES: i64 = SAMPLE_RATE as i64 / 4;
35const MAX_READS: u32 = 4096;
37const STABLE_REQS: u32 = 3;
40
41fn err(e: windows::core::Error) -> String {
42 format!("MF: {e}")
43}
44
45fn hns(t: f64) -> i64 {
46 (t.max(0.0) * HNS).round() as i64
47}
48
49fn init() -> Result<(), String> {
51 static MF: OnceLock<Result<(), String>> = OnceLock::new();
52 unsafe {
55 let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
56 }
57 MF.get_or_init(|| unsafe { MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET).map_err(err) }).clone()
58}
59
60struct SharedDeviceManager(IMFDXGIDeviceManager);
63unsafe impl Send for SharedDeviceManager {}
64unsafe impl Sync for SharedDeviceManager {}
65
66fn dxgi_device_manager() -> Option<IMFDXGIDeviceManager> {
70 static MANAGER: OnceLock<Option<SharedDeviceManager>> = OnceLock::new();
71 MANAGER
72 .get_or_init(|| unsafe {
73 let mut device: Option<ID3D11Device> = None;
74 D3D11CreateDevice(
75 None,
76 D3D_DRIVER_TYPE_HARDWARE,
77 Default::default(),
78 D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
79 None,
80 D3D11_SDK_VERSION,
81 Some(&mut device),
82 None,
83 None,
84 )
85 .ok()?;
86 let device = device?;
87 let mt: ID3D11Multithread = device.cast().ok()?;
89 let _ = mt.SetMultithreadProtected(true);
90 let mut token = 0u32;
91 let mut manager: Option<IMFDXGIDeviceManager> = None;
92 MFCreateDXGIDeviceManager(&mut token, &mut manager).ok()?;
93 let manager = manager?;
94 manager.ResetDevice(&device, token).ok()?;
95 Some(SharedDeviceManager(manager))
96 })
97 .as_ref()
98 .map(|m| m.0.clone())
99}
100
101const DXVA_MIN_PIXELS: u64 = 1280 * 720;
105
106fn open_reader(path: &str, dxva: bool) -> Result<IMFSourceReader, String> {
107 if super::is_image_path(path) {
108 return Err("MF: images are decoded by ffmpeg".into());
109 }
110 if matches!(super::ext(path).as_str(), "ts" | "m2ts" | "mts" | "m2t" | "mpg" | "mpeg" | "vob") {
113 return Err("MF: MPEG-TS/PS seeks are keyframe-coarse, use ffmpeg".into());
114 }
115 init()?;
116 unsafe {
117 let mut attrs = None;
118 MFCreateAttributes(&mut attrs, 2).map_err(err)?;
119 let attrs = attrs.ok_or("MF: MFCreateAttributes returned null")?;
120 attrs.SetUINT32(&MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, 1).map_err(err)?;
121 attrs.SetUINT32(&MF_XVP_DISABLE_FRC, 1).map_err(err)?;
124 if dxva {
127 if let Some(manager) = dxgi_device_manager() {
128 let _ = attrs.SetUnknown(&MF_SOURCE_READER_D3D_MANAGER, &manager);
129 }
130 }
131 let url = HSTRING::from(path);
132 let reader = MFCreateSourceReaderFromURL(&url, &attrs).map_err(err)?;
133 reader.SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS.0 as u32, false).map_err(err)?;
134 Ok(reader)
135 }
136}
137
138struct StreamInfo {
139 index: u32,
141 major: GUID,
142 ty: IMFMediaType,
143 language: String,
144 title: String,
145}
146
147fn streams(reader: &IMFSourceReader) -> Vec<StreamInfo> {
150 let mut v = Vec::new();
151 for i in 0..64u32 {
152 let Ok(ty) = (unsafe { reader.GetNativeMediaType(i, 0) }) else {
153 break;
154 };
155 let major = unsafe { ty.GetGUID(&MF_MT_MAJOR_TYPE) }.unwrap_or(GUID_NULL);
156 v.push(StreamInfo { index: i, major, ty, language: String::new(), title: String::new() });
157 }
158 unsafe {
159 let mut p = std::ptr::null_mut();
160 let r =
161 reader.GetServiceForStream(MF_SOURCE_READER_MEDIASOURCE.0 as u32, &GUID_NULL, &IMFMediaSource::IID, &mut p);
162 if r.is_ok() && !p.is_null() {
163 let src = IMFMediaSource::from_raw(p);
164 if let Ok(pd) = src.CreatePresentationDescriptor() {
165 for s in v.iter_mut() {
166 let mut sel = BOOL(0);
167 let mut sd = None;
168 let _ = pd.GetStreamDescriptorByIndex(s.index, &mut sel, &mut sd);
169 if let Some(sd) = sd {
170 s.language = get_string(&sd, &MF_SD_LANGUAGE);
171 s.title = get_string(&sd, &MF_SD_STREAM_NAME);
172 }
173 }
174 }
175 }
176 }
177 if is_mp4(reader) {
180 v.reverse();
181 }
182 v
183}
184
185fn is_mp4(reader: &IMFSourceReader) -> bool {
187 let mime = unsafe { reader.GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE.0 as u32, &MF_PD_MIME_TYPE) }
188 .map(|pv| pv.to_string())
189 .unwrap_or_default();
190 mime.ends_with("/mp4") || mime == "video/quicktime"
191}
192
193fn duration_secs(reader: &IMFSourceReader) -> f64 {
194 unsafe { reader.GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE.0 as u32, &MF_PD_DURATION) }
195 .ok()
196 .and_then(|pv| u64::try_from(&pv).ok())
197 .map(|d| d as f64 / HNS)
198 .unwrap_or(0.0)
199}
200
201fn get_string(a: &IMFAttributes, key: &GUID) -> String {
202 unsafe {
203 let Ok(len) = a.GetStringLength(key) else {
204 return String::new();
205 };
206 let mut buf = vec![0u16; len as usize + 1];
207 if a.GetString(key, &mut buf, None).is_err() {
208 return String::new();
209 }
210 String::from_utf16_lossy(&buf[..len as usize])
211 }
212}
213
214fn codec_name(sub: GUID) -> &'static str {
216 const CODECS: &[(GUID, &str)] = &[
217 (MFVideoFormat_H264, "h264"),
218 (MFVideoFormat_HEVC, "hevc"),
219 (MFVideoFormat_H265, "hevc"),
220 (MFVideoFormat_VP90, "vp9"),
221 (MFVideoFormat_VP80, "vp8"),
222 (MFVideoFormat_AV1, "av1"),
223 (MFVideoFormat_MPEG2, "mpeg2video"),
224 (MFVideoFormat_MP4V, "mpeg4"),
225 (MFVideoFormat_WMV3, "wmv3"),
226 (MFVideoFormat_MJPG, "mjpeg"),
227 (MFAudioFormat_AAC, "aac"),
228 (MFAudioFormat_MP3, "mp3"),
229 (MFAudioFormat_Opus, "opus"),
230 (MFAudioFormat_FLAC, "flac"),
231 (MFAudioFormat_Vorbis, "vorbis"),
232 (MFAudioFormat_ALAC, "alac"),
233 (MFAudioFormat_Dolby_AC3, "ac3"),
234 (MFAudioFormat_Dolby_DDPlus, "eac3"),
235 (MFAudioFormat_WMAudioV8, "wmav2"),
236 (MFAudioFormat_PCM, "pcm"),
237 (MFAudioFormat_Float, "pcm"),
238 ];
239 CODECS.iter().find(|(g, _)| *g == sub).map_or("", |(_, n)| n)
240}
241
242pub fn probe(path: &str) -> Result<Asset, String> {
243 let reader = open_reader(path, false)?; let mut asset = Asset {
245 id: 0,
246 path: path.to_string(),
247 kind: ClipKind::Audio,
248 duration: duration_secs(&reader),
249 width: 0,
250 height: 0,
251 fps: 0.0,
252 audio_streams: Vec::new(),
253 codec: String::new(),
254 folder: String::new(),
255 tags: Vec::new(),
256 label: 0,
257 description: String::new(),
258 };
259 let mut has_video = false;
260 for s in streams(&reader) {
261 let sub = unsafe { s.ty.GetGUID(&MF_MT_SUBTYPE) }.unwrap_or(GUID_NULL);
262 if s.major == MFMediaType_Video && !has_video {
263 has_video = true;
264 let size = unsafe { s.ty.GetUINT64(&MF_MT_FRAME_SIZE) }.unwrap_or(0);
265 asset.width = (size >> 32) as u32;
266 asset.height = size as u32;
267 if unsafe { s.ty.GetUINT32(&MF_MT_VIDEO_ROTATION) }.unwrap_or(0) % 180 == 90 {
269 std::mem::swap(&mut asset.width, &mut asset.height);
270 }
271 asset.fps = frame_rate(&s.ty);
272 asset.codec = codec_name(sub).into();
273 } else if s.major == MFMediaType_Audio {
274 asset.audio_streams.push(AudioStreamInfo {
275 index: asset.audio_streams.len(),
276 channels: unsafe { s.ty.GetUINT32(&MF_MT_AUDIO_NUM_CHANNELS) }.unwrap_or(0),
277 sample_rate: unsafe { s.ty.GetUINT32(&MF_MT_AUDIO_SAMPLES_PER_SECOND) }.unwrap_or(0),
278 language: s.language,
279 title: s.title,
280 codec: codec_name(sub).into(),
281 });
282 }
283 }
284 if has_video {
285 asset.kind = ClipKind::Video;
286 } else if let Some(a) = asset.audio_streams.first() {
287 asset.codec = a.codec.clone();
288 } else {
289 return Err("MF: no audio or video streams".into());
290 }
291 Ok(asset)
292}
293
294fn frame_rate(ty: &IMFMediaType) -> f64 {
295 let r = unsafe { ty.GetUINT64(&MF_MT_FRAME_RATE) }.unwrap_or(0);
296 let (num, den) = ((r >> 32) as u32, r as u32);
297 if num > 0 && den > 0 {
298 num as f64 / den as f64
299 } else {
300 0.0
301 }
302}
303
304struct MfVideo {
307 reader: IMFSourceReader,
308 stream: u32,
309 native_w: u32,
313 native_h: u32,
314 scale_ok: bool,
317 req_last: (u32, u32),
320 req_streak: u32,
321 negotiated: bool,
323 width: u32,
324 height: u32,
325 stride: i32,
327 frame_hns: i64,
329 native: Vec<u8>,
331 have: bool,
332 pts: i64,
335 end: i64,
336 rpts: i64,
337 last_end: Option<i64>,
339 eof_at: Option<i64>,
341 origin: i64,
344 mp4: bool,
346 scaled: Vec<u8>,
348 svalid: bool,
349 sw: u32,
350 sh: u32,
351 xs: Vec<u32>,
353 ys: Vec<u32>,
354 acc: Vec<u32>,
355 tab_key: (u32, u32, u32, u32),
356}
357
358unsafe impl Send for MfVideo {}
361
362pub fn open_video(path: &str) -> Result<Box<dyn VideoSource>, String> {
363 let mut reader = open_reader(path, false)?;
364 {
367 let vs = streams(&reader).into_iter().find(|s| s.major == MFMediaType_Video).ok_or("MF: no video stream")?;
368 let size = unsafe { vs.ty.GetUINT64(&MF_MT_FRAME_SIZE) }.unwrap_or(0);
369 if ((size >> 32) & 0xffff_ffff) * (size & 0xffff_ffff) >= DXVA_MIN_PIXELS {
370 if let Ok(r) = open_reader(path, true) {
371 reader = r;
372 }
373 }
374 }
375 let vs = streams(&reader).into_iter().find(|s| s.major == MFMediaType_Video).ok_or("MF: no video stream")?;
376 let (stream, nat) = (vs.index, vs.ty);
377 let mp4 = is_mp4(&reader);
378 unsafe {
379 reader.SetStreamSelection(stream, true).map_err(err)?;
380 let mt = MFCreateMediaType().map_err(err)?;
381 mt.SetGUID(&MF_MT_MAJOR_TYPE, &MFMediaType_Video).map_err(err)?;
382 mt.SetGUID(&MF_MT_SUBTYPE, &MFVideoFormat_RGB32).map_err(err)?;
383 reader.SetCurrentMediaType(stream, None, &mt).map_err(err)?;
384 }
385 let fps = frame_rate(&nat);
386 let mut v = MfVideo {
387 reader,
388 stream,
389 native_w: 0,
390 native_h: 0,
391 scale_ok: true,
392 req_last: (0, 0),
393 req_streak: 0,
394 negotiated: false,
395 width: 0,
396 height: 0,
397 stride: 0,
398 frame_hns: if fps > 0.0 { (HNS / fps) as i64 } else { 333_333 },
399 native: Vec::new(),
400 have: false,
401 pts: 0,
402 end: 0,
403 rpts: 0,
404 last_end: None,
405 eof_at: None,
406 origin: 0,
407 mp4,
408 scaled: Vec::new(),
409 svalid: false,
410 sw: 0,
411 sh: 0,
412 xs: Vec::new(),
413 ys: Vec::new(),
414 acc: Vec::new(),
415 tab_key: (0, 0, 0, 0),
416 };
417 if !v.refresh_type() {
418 return Err("MF: cannot read video output type".into());
419 }
420 (v.native_w, v.native_h) = (v.width, v.height);
421 if !v.read_until(0) {
424 return Err("MF: cannot decode first video frame".into());
425 }
426 v.origin = v.rpts.max(0);
427 v.pts = v.rpts;
428 Ok(Box::new(v))
429}
430
431impl MfVideo {
432 fn refresh_type(&mut self) -> bool {
434 let Ok(cur) = (unsafe { self.reader.GetCurrentMediaType(self.stream) }) else {
435 return false;
436 };
437 let size = unsafe { cur.GetUINT64(&MF_MT_FRAME_SIZE) }.unwrap_or(0);
438 let (w, h) = ((size >> 32) as u32, size as u32);
439 if w == 0 || h == 0 || w > 16384 || h > 16384 {
440 return false;
441 }
442 self.width = w;
443 self.height = h;
444 self.stride = unsafe { cur.GetUINT32(&MF_MT_DEFAULT_STRIDE) }.map(|s| s as i32).unwrap_or(0);
445 self.native.resize((w * h * 4) as usize, 0);
446 self.have = false;
447 self.svalid = false;
448 true
449 }
450
451 fn ensure_decode_size(&mut self, req_w: u32, req_h: u32) {
464 if !self.scale_ok || self.native_w < 4 || self.native_h < 4 {
465 return;
466 }
467 let t = (((req_w + 1) & !1).clamp(2, self.native_w & !1), ((req_h + 1) & !1).clamp(2, self.native_h & !1));
469 if t == (self.width, self.height) {
470 self.req_last = t;
471 self.req_streak = STABLE_REQS; return;
473 }
474 if t == self.req_last {
475 self.req_streak = self.req_streak.saturating_add(1);
476 } else {
477 self.req_last = t;
478 self.req_streak = 1;
479 }
480 if self.req_streak < STABLE_REQS && self.negotiated {
481 return; }
483 self.negotiated = true;
484 let negotiated = unsafe {
485 (|| -> Result<(), String> {
486 let mt = MFCreateMediaType().map_err(err)?;
487 mt.SetGUID(&MF_MT_MAJOR_TYPE, &MFMediaType_Video).map_err(err)?;
488 mt.SetGUID(&MF_MT_SUBTYPE, &MFVideoFormat_RGB32).map_err(err)?;
489 mt.SetUINT64(&MF_MT_FRAME_SIZE, ((t.0 as u64) << 32) | t.1 as u64).map_err(err)?;
490 self.reader.SetCurrentMediaType(self.stream, None, &mt).map_err(err)
491 })()
492 };
493 if negotiated.is_err() || !self.refresh_type() {
496 self.scale_ok = false; }
498 }
499
500 fn seek(&mut self, tt: i64) -> bool {
501 let pv = PROPVARIANT::from(if self.mp4 { tt - self.origin } else { tt });
502 let ok = unsafe { self.reader.SetCurrentPosition(&GUID_NULL, &pv) }.is_ok();
503 self.have = false;
504 self.last_end = None;
505 ok
506 }
507
508 fn read_until(&mut self, tt: i64) -> bool {
510 for _ in 0..MAX_READS {
511 let mut flags = 0u32;
512 let mut ts = 0i64;
513 let mut sample = None;
514 let r = unsafe {
515 self.reader.ReadSample(self.stream, 0, None, Some(&mut flags), Some(&mut ts), Some(&mut sample))
516 };
517 if r.is_err() || flags & MF_SOURCE_READERF_ERROR.0 as u32 != 0 {
518 return false;
519 }
520 if flags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED.0 as u32 != 0 && !self.refresh_type() {
521 return false;
522 }
523 if flags & MF_SOURCE_READERF_ENDOFSTREAM.0 as u32 != 0 {
524 let e = self.last_end.unwrap_or(tt);
525 self.eof_at = Some(self.eof_at.map_or(e, |x| x.min(e)));
526 return false;
527 }
528 let Some(sample) = sample else { continue }; let dur = unsafe { sample.GetSampleDuration() }.unwrap_or(0);
530 let end = ts + if dur > 0 { dur } else { self.frame_hns };
531 self.last_end = Some(end);
532 if end <= tt {
533 continue;
534 }
535 if !self.copy_sample(&sample) {
536 return false;
537 }
538 self.have = true;
539 self.rpts = ts;
540 self.pts = ts.min(tt);
541 self.end = end;
542 self.svalid = false;
543 return true;
544 }
545 false
546 }
547
548 fn copy_sample(&mut self, sample: &IMFSample) -> bool {
550 let (w, h) = (self.width as usize, self.height as usize);
551 unsafe {
552 let Ok(buf) = sample.ConvertToContiguousBuffer() else {
553 return false;
554 };
555 if let Ok(b2) = buf.cast::<IMF2DBuffer>() {
556 let mut p: *mut u8 = std::ptr::null_mut();
557 let mut pitch = 0i32;
558 if b2.Lock2D(&mut p, &mut pitch).is_err() || p.is_null() {
559 return false;
560 }
561 convert_rows(p, pitch as isize, w, h, &mut self.native);
563 let _ = b2.Unlock2D();
564 } else {
565 let mut p: *mut u8 = std::ptr::null_mut();
566 let mut len = 0u32;
567 if buf.Lock(&mut p, None, Some(&mut len)).is_err() || p.is_null() {
568 return false;
569 }
570 let stride = if self.stride != 0 { self.stride as isize } else { (w * 4) as isize };
571 if (len as usize) < h * stride.unsigned_abs() {
572 let _ = buf.Unlock();
573 return false;
574 }
575 let top = if stride < 0 { p.offset((h as isize - 1) * -stride) } else { p };
577 convert_rows(top, stride, w, h, &mut self.native);
578 let _ = buf.Unlock();
579 }
580 }
581 true
582 }
583
584 fn emit(&mut self, w: u32, h: u32, out: &mut Frame) {
586 out.resize(w, h);
587 out.pts = (self.rpts - self.origin) as f64 / HNS;
588 if w == self.width && h == self.height {
589 out.rgba.copy_from_slice(&self.native);
590 return;
591 }
592 if !(self.svalid && self.sw == w && self.sh == h) {
593 self.rescale(w, h);
594 }
595 out.rgba.copy_from_slice(&self.scaled);
596 }
597
598 fn rescale(&mut self, w: u32, h: u32) {
599 self.scaled.resize((w * h * 4) as usize, 0);
600 if w <= self.width && h <= self.height {
601 let key = (self.width, self.height, w, h);
602 if self.tab_key != key {
603 self.tab_key = key;
604 self.xs = (0..=w).map(|i| (i as u64 * self.width as u64 / w as u64) as u32).collect();
605 self.ys = (0..=h).map(|j| (j as u64 * self.height as u64 / h as u64) as u32).collect();
606 self.acc.resize(w as usize * 3, 0);
607 }
608 box_down(&self.native, self.width, &self.xs, &self.ys, &mut self.acc, &mut self.scaled);
609 } else {
610 bilinear(&self.native, self.width, self.height, w, h, &mut self.scaled);
612 }
613 self.svalid = true;
614 self.sw = w;
615 self.sh = h;
616 }
617}
618
619unsafe fn convert_rows(top: *const u8, pitch: isize, w: usize, h: usize, dst: &mut [u8]) {
621 for (y, drow) in dst.chunks_exact_mut(w * 4).take(h).enumerate() {
622 let src = std::slice::from_raw_parts(top.offset(y as isize * pitch), w * 4);
623 for (s, d) in src.chunks_exact(4).zip(drow.chunks_exact_mut(4)) {
624 d[0] = s[2];
625 d[1] = s[1];
626 d[2] = s[0];
627 d[3] = 255;
628 }
629 }
630}
631
632fn box_down(src: &[u8], sw: u32, xs: &[u32], ys: &[u32], acc: &mut [u32], dst: &mut [u8]) {
634 let (sw, w, h) = (sw as usize, xs.len() - 1, ys.len() - 1);
635 for (j, drow) in dst.chunks_exact_mut(w * 4).take(h).enumerate() {
636 acc.fill(0);
637 let (y0, y1) = (ys[j] as usize, ys[j + 1] as usize);
638 for row in src[y0 * sw * 4..y1 * sw * 4].chunks_exact(sw * 4) {
639 for (i, a) in acc.chunks_exact_mut(3).enumerate() {
640 let (x0, x1) = (xs[i] as usize, xs[i + 1] as usize);
641 for px in row[x0 * 4..x1 * 4].chunks_exact(4) {
642 a[0] += px[0] as u32;
643 a[1] += px[1] as u32;
644 a[2] += px[2] as u32;
645 }
646 }
647 }
648 let rows = (y1 - y0) as u32;
649 for (i, (d, a)) in drow.chunks_exact_mut(4).zip(acc.chunks_exact(3)).enumerate() {
650 let n = (rows * (xs[i + 1] - xs[i])).max(1);
651 d[0] = (a[0] / n) as u8;
652 d[1] = (a[1] / n) as u8;
653 d[2] = (a[2] / n) as u8;
654 d[3] = 255;
655 }
656 }
657}
658
659fn bilinear(src: &[u8], sw: u32, sh: u32, w: u32, h: u32, dst: &mut [u8]) {
660 let (sw, sh) = (sw as usize, sh as usize);
661 let px = |x: usize, y: usize| &src[(y * sw + x) * 4..(y * sw + x) * 4 + 4];
662 for (j, drow) in dst.chunks_exact_mut(w as usize * 4).enumerate() {
663 let fy = ((j as f32 + 0.5) * sh as f32 / h as f32 - 0.5).clamp(0.0, (sh - 1) as f32);
664 let (y0, wy) = (fy as usize, fy.fract());
665 let y1 = (y0 + 1).min(sh - 1);
666 for (i, d) in drow.chunks_exact_mut(4).enumerate() {
667 let fx = ((i as f32 + 0.5) * sw as f32 / w as f32 - 0.5).clamp(0.0, (sw - 1) as f32);
668 let (x0, wx) = (fx as usize, fx.fract());
669 let x1 = (x0 + 1).min(sw - 1);
670 let (a, b, c, e) = (px(x0, y0), px(x1, y0), px(x0, y1), px(x1, y1));
671 for k in 0..3 {
672 let top = a[k] as f32 + (b[k] as f32 - a[k] as f32) * wx;
673 let bot = c[k] as f32 + (e[k] as f32 - c[k] as f32) * wx;
674 d[k] = (top + (bot - top) * wy + 0.5) as u8;
675 }
676 d[3] = 255;
677 }
678 }
679}
680
681impl VideoSource for MfVideo {
682 fn size(&self) -> (u32, u32) {
683 (self.native_w, self.native_h)
684 }
685 fn frame_at(&mut self, t: f64, w: u32, h: u32, out: &mut Frame) -> bool {
686 if w == 0 || h == 0 {
687 return false;
688 }
689 self.ensure_decode_size(w, h);
690 let tt = hns(t) + 50_000 + self.origin;
693 if self.eof_at.is_some_and(|e| tt >= e) {
694 return false;
695 }
696 if !(self.have && tt >= self.pts && tt < self.end) {
697 let forward = self.have && tt >= self.end && tt < self.pts + VIDEO_FWD_HNS;
698 if (!forward && !self.seek(tt)) || !self.read_until(tt) {
699 return false;
700 }
701 }
702 self.emit(w, h, out);
703 true
704 }
705}
706
707#[derive(Default)]
711struct Resamp {
712 pos: f64,
714 last: [f32; 2],
715 primed: bool,
716}
717
718fn push_audio(src: &[f32], ch: usize, rate: u32, rs: &mut Resamp, fifo: &mut Vec<f32>) {
721 if ch == 0 || src.len() < ch {
722 return;
723 }
724 let n = src.len() / ch;
725 let rc = 1.min(ch - 1);
726 let frame = |k: usize| [src[k * ch], src[k * ch + rc]];
727 if rate == SAMPLE_RATE {
728 fifo.reserve(n * 2);
729 for k in 0..n {
730 fifo.extend_from_slice(&frame(k));
731 }
732 return;
733 }
734 if !rs.primed {
735 *rs = Resamp { pos: 1.0, last: frame(0), primed: true };
736 }
737 let step = rate as f64 / SAMPLE_RATE as f64;
739 let v = |k: usize| if k == 0 { rs.last } else { frame(k - 1) };
740 let mut pos = rs.pos;
741 while pos < n as f64 {
742 let k = pos as usize;
743 let f = (pos - k as f64) as f32;
744 let (a, b) = (v(k), v(k + 1));
745 fifo.push(a[0] + (b[0] - a[0]) * f);
746 fifo.push(a[1] + (b[1] - a[1]) * f);
747 pos += step;
748 }
749 rs.pos = pos - n as f64;
750 rs.last = frame(n - 1);
751}
752
753struct MfAudio {
754 reader: IMFSourceReader,
755 stream: u32,
756 duration: f64,
757 rate: u32,
759 ch: u32,
760 fifo: Vec<f32>,
762 fifo_pos: i64,
763 located: bool,
764 eof_at: Option<i64>,
766 origin: i64,
768 mp4: bool,
769 rs: Resamp,
770 tmp: Vec<f32>,
771}
772
773unsafe impl Send for MfAudio {}
775
776pub fn open_audio(path: &str, stream: usize) -> Result<Box<dyn AudioSource>, String> {
777 let reader = open_reader(path, false)?; let s = streams(&reader)
779 .into_iter()
780 .filter(|s| s.major == MFMediaType_Audio)
781 .nth(stream)
782 .ok_or_else(|| format!("MF: no audio stream {stream}"))?;
783 const FLAC_MP4: GUID = GUID { data1: 0x664C_6143, ..MFMPEG4Format_Base }; let sub = unsafe { s.ty.GetGUID(&MF_MT_SUBTYPE) }.unwrap_or(GUID_NULL);
787 if sub == MFAudioFormat_FLAC || sub == FLAC_MP4 {
788 return Err("MF: FLAC timestamps unreliable, use ffmpeg".into());
789 }
790 let idx = s.index;
791 let mp4 = is_mp4(&reader);
792 let duration = duration_secs(&reader);
793 let (rate, ch) = unsafe {
794 reader.SetStreamSelection(idx, true).map_err(err)?;
795 let full = MFCreateMediaType().map_err(err)?;
796 full.SetGUID(&MF_MT_MAJOR_TYPE, &MFMediaType_Audio).map_err(err)?;
797 full.SetGUID(&MF_MT_SUBTYPE, &MFAudioFormat_Float).map_err(err)?;
798 full.SetUINT32(&MF_MT_AUDIO_BITS_PER_SAMPLE, 32).map_err(err)?;
799 full.SetUINT32(&MF_MT_AUDIO_NUM_CHANNELS, CHANNELS as u32).map_err(err)?;
800 full.SetUINT32(&MF_MT_AUDIO_SAMPLES_PER_SECOND, SAMPLE_RATE).map_err(err)?;
801 full.SetUINT32(&MF_MT_AUDIO_BLOCK_ALIGNMENT, 4 * CHANNELS as u32).map_err(err)?;
802 full.SetUINT32(&MF_MT_AUDIO_AVG_BYTES_PER_SECOND, 4 * CHANNELS as u32 * SAMPLE_RATE).map_err(err)?;
803 if reader.SetCurrentMediaType(idx, None, &full).is_err() {
804 let mt = MFCreateMediaType().map_err(err)?;
806 mt.SetGUID(&MF_MT_MAJOR_TYPE, &MFMediaType_Audio).map_err(err)?;
807 mt.SetGUID(&MF_MT_SUBTYPE, &MFAudioFormat_Float).map_err(err)?;
808 reader.SetCurrentMediaType(idx, None, &mt).map_err(err)?;
809 }
810 audio_format(&reader, idx)?
811 };
812 let mut a = MfAudio {
813 reader,
814 stream: idx,
815 duration,
816 rate,
817 ch,
818 fifo: Vec::new(),
819 fifo_pos: 0,
820 located: false,
821 eof_at: None,
822 origin: 0,
823 mp4,
824 rs: Resamp::default(),
825 tmp: Vec::new(),
826 };
827 let mut reads = 0;
830 while !a.located {
831 reads += 1;
832 if reads > MAX_READS || !a.decode_more(0) {
833 return Err("MF: cannot decode first audio sample".into());
834 }
835 }
836 a.origin = a.fifo_pos;
837 Ok(Box::new(a))
838}
839
840fn audio_format(reader: &IMFSourceReader, stream: u32) -> Result<(u32, u32), String> {
842 let cur = unsafe { reader.GetCurrentMediaType(stream) }.map_err(err)?;
843 let rate = unsafe { cur.GetUINT32(&MF_MT_AUDIO_SAMPLES_PER_SECOND) }.map_err(err)?;
844 let ch = unsafe { cur.GetUINT32(&MF_MT_AUDIO_NUM_CHANNELS) }.map_err(err)?;
845 if rate == 0 || ch == 0 || ch > 64 {
846 return Err(format!("MF: bad audio format {rate} Hz / {ch} ch"));
847 }
848 Ok((rate, ch))
849}
850
851impl MfAudio {
852 fn fifo_end(&self) -> i64 {
853 self.fifo_pos + (self.fifo.len() / CHANNELS) as i64
854 }
855
856 fn seek(&mut self, f0: i64) -> bool {
857 let f = f0 - if self.mp4 { self.origin } else { 0 } - AUDIO_PREROLL_FRAMES;
860 let pv = PROPVARIANT::from(f.max(0) * HNS as i64 / SAMPLE_RATE as i64);
861 let ok = unsafe { self.reader.SetCurrentPosition(&GUID_NULL, &pv) }.is_ok();
862 self.fifo.clear();
863 self.located = false;
864 self.rs.primed = false;
865 ok
866 }
867
868 fn mark_eof(&mut self, f0: i64) {
869 let e = if self.located { self.fifo_end() } else { f0 };
870 self.eof_at = Some(self.eof_at.map_or(e, |x| x.min(e)));
871 }
872
873 fn decode_more(&mut self, f0: i64) -> bool {
875 let mut flags = 0u32;
876 let mut ts = 0i64;
877 let mut sample = None;
878 let r =
879 unsafe { self.reader.ReadSample(self.stream, 0, None, Some(&mut flags), Some(&mut ts), Some(&mut sample)) };
880 if r.is_err() || flags & (MF_SOURCE_READERF_ERROR.0 | MF_SOURCE_READERF_ENDOFSTREAM.0) as u32 != 0 {
881 self.mark_eof(f0);
883 return false;
884 }
885 if flags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED.0 as u32 != 0 {
886 match audio_format(&self.reader, self.stream) {
887 Ok((rate, ch)) => {
888 self.rate = rate;
889 self.ch = ch;
890 self.rs.primed = false;
891 }
892 Err(_) => {
893 self.mark_eof(f0);
894 return false;
895 }
896 }
897 }
898 let Some(sample) = sample else { return true }; unsafe {
900 let Ok(buf) = sample.ConvertToContiguousBuffer() else {
901 return true;
902 };
903 let mut p: *mut u8 = std::ptr::null_mut();
904 let mut len = 0u32;
905 if buf.Lock(&mut p, None, Some(&mut len)).is_err() || p.is_null() {
906 return true;
907 }
908 let bytes = std::slice::from_raw_parts(p, len as usize);
909 self.tmp.clear();
910 self.tmp.extend(bytes.chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])));
911 let _ = buf.Unlock();
912 }
913 if !self.located {
914 self.fifo_pos = (ts as f64 * SAMPLE_RATE as f64 / HNS).round() as i64;
915 self.located = true;
916 }
917 push_audio(&self.tmp, self.ch as usize, self.rate, &mut self.rs, &mut self.fifo);
918 true
919 }
920}
921
922impl AudioSource for MfAudio {
923 fn duration(&self) -> f64 {
924 self.duration
925 }
926 fn read_at(&mut self, t: f64, out: &mut [f32]) {
927 out.fill(0.0);
928 let n = (out.len() / CHANNELS) as i64;
929 let f0 = (t.max(0.0) * SAMPLE_RATE as f64).round() as i64 + self.origin;
930 let f1 = f0 + n;
931 if n == 0 || self.eof_at.is_some_and(|e| f0 >= e) {
932 return;
933 }
934 if (!self.located || f0 < self.fifo_pos || f0 > self.fifo_end() + AUDIO_FWD_FRAMES) && !self.seek(f0) {
935 return;
936 }
937 let mut reads = 0;
938 while !(self.located && self.fifo_end() >= f1) && reads < MAX_READS {
939 reads += 1;
940 if !self.decode_more(f0) {
941 break;
942 }
943 }
944 if !self.located {
945 return;
946 }
947 let (a, b) = (f0.max(self.fifo_pos), f1.min(self.fifo_end()));
948 if b > a {
949 let so = ((a - self.fifo_pos) as usize) * CHANNELS;
950 let len = ((b - a) as usize) * CHANNELS;
951 let doff = ((a - f0) as usize) * CHANNELS;
952 out[doff..doff + len].copy_from_slice(&self.fifo[so..so + len]);
953 }
954 let drop = ((f0 - self.fifo_pos).max(0) as usize).min(self.fifo.len() / CHANNELS);
956 if drop > 0 {
957 self.fifo.drain(..drop * CHANNELS);
958 self.fifo_pos += drop as i64;
959 }
960 }
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966 use std::path::PathBuf;
967 use std::process::Command;
968 use std::time::Instant;
969
970 fn media() -> Option<String> {
972 static P: OnceLock<Option<String>> = OnceLock::new();
973 P.get_or_init(|| {
974 let dir = std::env::temp_dir().join("simple-editor-mf-test");
975 std::fs::create_dir_all(&dir).ok()?;
976 let out: PathBuf = dir.join("test.mp4");
977 let st = Command::new("ffmpeg")
978 .args(["-v", "error", "-y"])
979 .args(["-f", "lavfi", "-i", "color=red:s=320x240:d=2,drawbox=y=120:h=120:color=blue:t=fill"])
980 .args(["-f", "lavfi", "-i", "color=lime:s=320x240:d=2"])
981 .args(["-f", "lavfi", "-i", "sine=frequency=440:duration=4,volume=4"])
982 .args(["-f", "lavfi", "-i", "sine=frequency=880:duration=4,volume=4"])
983 .args(["-filter_complex", "[0:v][1:v]concat=n=2:v=1[v]", "-map", "[v]", "-map", "2:a", "-map", "3:a"])
984 .args(["-r", "30", "-pix_fmt", "yuv420p", "-c:v", "libx264", "-c:a", "aac"])
985 .args(["-metadata:s:a:0", "language=eng", "-metadata:s:a:1", "title=Music"])
986 .arg(&out)
987 .status()
988 .ok()?;
989 st.success().then(|| out.to_string_lossy().into_owned())
990 })
991 .clone()
992 }
993
994 fn derive(name: &str, pre: &[&str], post: &[&str]) -> Option<String> {
996 let src = media()?;
997 let out = PathBuf::from(&src).with_file_name(format!("{}-{name}", std::process::id()));
998 let st = Command::new("ffmpeg")
999 .args(["-v", "error", "-y"])
1000 .args(pre)
1001 .args(["-i", &src])
1002 .args(post)
1003 .arg(&out)
1004 .status()
1005 .ok()?;
1006 st.success().then(|| out.to_string_lossy().into_owned())
1007 }
1008
1009 fn px(f: &Frame, x: u32, y: u32) -> [u8; 4] {
1010 let i = ((y * f.width + x) * 4) as usize;
1011 [f.rgba[i], f.rgba[i + 1], f.rgba[i + 2], f.rgba[i + 3]]
1012 }
1013 fn is_red(p: [u8; 4]) -> bool {
1014 p[0] > 200 && p[1] < 70 && p[2] < 70
1015 }
1016 fn is_green(p: [u8; 4]) -> bool {
1017 p[0] < 70 && p[1] > 200 && p[2] < 70
1018 }
1019 fn is_blue(p: [u8; 4]) -> bool {
1020 p[0] < 70 && p[1] < 70 && p[2] > 200
1021 }
1022 fn zero_crossings(stereo: &[f32]) -> usize {
1023 let l: Vec<f32> = stereo.chunks_exact(2).map(|c| c[0]).collect();
1024 l.windows(2).filter(|w| (w[0] < 0.0) != (w[1] < 0.0)).count()
1025 }
1026 fn rms(s: &[f32]) -> f64 {
1027 (s.iter().map(|v| (*v as f64) * (*v as f64)).sum::<f64>() / s.len().max(1) as f64).sqrt()
1028 }
1029
1030 #[test]
1031 fn probe_reports_streams() {
1032 let Some(p) = media() else {
1033 eprintln!("ffmpeg missing; skipped");
1034 return;
1035 };
1036 let a = probe(&p).expect("probe");
1037 assert_eq!(a.kind, ClipKind::Video);
1038 assert!((a.duration - 4.0).abs() < 0.1, "duration {}", a.duration);
1039 assert_eq!((a.width, a.height), (320, 240));
1040 assert!((a.fps - 30.0).abs() < 0.05, "fps {}", a.fps);
1041 assert_eq!(a.audio_streams.len(), 2);
1042 assert_eq!(a.audio_streams[0].index, 0);
1043 assert_eq!(a.audio_streams[1].index, 1);
1044 assert_eq!(a.codec, "h264");
1045 eprintln!("audio streams: {:?}", a.audio_streams);
1047 assert_eq!(a.audio_streams[0].channels, 1);
1048 assert_eq!(a.audio_streams[0].sample_rate, 44100);
1049 assert!(probe("C:/definitely/missing.mp4").is_err());
1050 assert!(probe("C:/x.png").is_err());
1051 }
1052
1053 #[test]
1054 fn video_frames_seek_scale() {
1055 let Some(p) = media() else {
1056 eprintln!("ffmpeg missing; skipped");
1057 return;
1058 };
1059 let mut v = open_video(&p).expect("open_video");
1060 assert_eq!(v.size(), (320, 240));
1061 let mut f = Frame::default();
1062 assert!(v.frame_at(0.5, 320, 240, &mut f));
1063 assert_eq!((f.width, f.height), (320, 240));
1064 assert!(is_red(px(&f, 10, 10)), "top-left {:?}", px(&f, 10, 10));
1065 assert!(is_blue(px(&f, 10, 230)), "bottom-left {:?} (orientation)", px(&f, 10, 230));
1066 assert!(v.frame_at(2.5, 320, 240, &mut f));
1067 assert!(is_green(px(&f, 160, 120)), "{:?}", px(&f, 160, 120));
1068 assert!(v.frame_at(0.5, 320, 240, &mut f), "seek back");
1069 assert!(is_red(px(&f, 10, 10)));
1070 assert!(v.frame_at(1.0, 160, 120, &mut f));
1071 assert_eq!((f.width, f.height), (160, 120));
1072 assert!(is_red(px(&f, 5, 5)));
1073 assert!(is_blue(px(&f, 5, 115)));
1074 assert!(v.frame_at(1.0, 400, 300, &mut f), "upscale");
1075 assert_eq!((f.width, f.height), (400, 300));
1076 assert!(is_red(px(&f, 5, 5)));
1077 assert!(is_blue(px(&f, 5, 295)));
1078 assert!(!v.frame_at(10.0, 320, 240, &mut f), "past end");
1079 assert!(!v.frame_at(4.5, 320, 240, &mut f), "past end (cached eof)");
1080 assert!(v.frame_at(3.9, 320, 240, &mut f), "near end still decodes");
1081 assert!(is_green(px(&f, 160, 120)));
1082 let t0 = Instant::now();
1084 let mut n = 0;
1085 for i in 0..100 {
1086 if v.frame_at(i as f64 / 30.0, 320, 240, &mut f) {
1087 n += 1;
1088 }
1089 }
1090 let dt = t0.elapsed();
1091 eprintln!("100 sequential 320x240 frames: {dt:?} ({n} ok)");
1092 assert_eq!(n, 100);
1093 assert!(dt.as_secs_f64() < 1.0, "too slow: {dt:?}");
1094 let t0 = Instant::now();
1095 for _ in 0..100 {
1096 assert!(v.frame_at(1.5, 160, 120, &mut f));
1097 }
1098 eprintln!("100 repeated frame_at(1.5) @160x120: {:?}", t0.elapsed());
1099 }
1100
1101 #[test]
1104 #[ignore]
1105 fn bench_4k_preview() {
1106 let dir = std::env::temp_dir().join("se-bench");
1107 let _ = std::fs::create_dir_all(&dir);
1108 let p = dir.join("4k30.mp4");
1109 if !p.exists() {
1110 let ok = std::process::Command::new("ffmpeg")
1111 .args(["-y", "-f", "lavfi", "-i", "testsrc2=size=3840x2160:rate=30", "-t", "2"])
1112 .args(["-c:v", "libx264", "-preset", "veryfast", "-pix_fmt", "yuv420p"])
1113 .arg(&p)
1114 .status()
1115 .map(|s| s.success())
1116 .unwrap_or(false);
1117 if !ok {
1118 eprintln!("ffmpeg missing; skipped");
1119 return;
1120 }
1121 }
1122 let path = p.to_string_lossy().to_string();
1123 let t0 = Instant::now();
1124 let mut v = open_video(&path).expect("open 4k");
1125 eprintln!("open_video(4K): {:?}", t0.elapsed());
1126 let mut f = Frame::default();
1127 for (dw, dh, label) in [(1280u32, 720u32, "1280x720 preview"), (3840, 2160, "native 4K")] {
1128 assert!(v.frame_at(0.0, dw, dh, &mut f), "first frame at {label}");
1129 let t0 = Instant::now();
1130 let mut n = 0;
1131 for i in 1..60 {
1132 if v.frame_at(i as f64 / 30.0, dw, dh, &mut f) {
1133 n += 1;
1134 }
1135 }
1136 let dt = t0.elapsed();
1137 eprintln!(
1138 "4K -> {label}: {n} frames in {dt:?} ({:.1} ms/frame)",
1139 dt.as_secs_f64() * 1000.0 / n.max(1) as f64
1140 );
1141 assert_eq!(n, 59);
1142 v.frame_at(0.0, dw, dh, &mut f); }
1144 }
1145
1146 #[test]
1147 fn audio_streams_read() {
1148 let Some(p) = media() else {
1149 eprintln!("ffmpeg missing; skipped");
1150 return;
1151 };
1152 let mut a = open_audio(&p, 0).expect("open_audio 0");
1153 assert!((a.duration() - 4.0).abs() < 0.1);
1154 let mut buf = vec![0f32; 4800 * 2];
1155 a.read_at(1.0, &mut buf);
1156 let r = rms(&buf);
1157 assert!((0.1..=1.0).contains(&r), "rms {r}");
1158 assert!(buf.chunks_exact(2).all(|c| (c[0] - c[1]).abs() < 1e-4));
1160 let last = buf[buf.len() - 2];
1162 a.read_at(1.0 + 4800.0 / 48000.0, &mut buf);
1163 assert!((buf[0] - last).abs() < 0.15, "discontinuity {last} -> {}", buf[0]);
1164 assert!((0.1..=1.0).contains(&rms(&buf)));
1165 let mut again = vec![0f32; 4800 * 2];
1167 a.read_at(1.0 + 4800.0 / 48000.0, &mut again);
1168 assert_eq!(buf, again);
1169 a.read_at(10.0, &mut buf);
1170 assert!(buf.iter().all(|v| *v == 0.0), "past end must be silent");
1171 a.read_at(0.5, &mut buf);
1172 assert!((0.1..=1.0).contains(&rms(&buf)), "seek back after eof");
1173 a.read_at(3.95, &mut buf);
1175 assert!(buf[buf.len() - 2..].iter().all(|v| *v == 0.0));
1176 a.read_at(1.0, &mut buf);
1178 let zc0 = zero_crossings(&buf);
1179 let mut b = open_audio(&p, 1).expect("open_audio 1");
1180 b.read_at(2.0, &mut buf);
1181 assert!((0.1..=1.0).contains(&rms(&buf)), "stream 1 rms");
1182 let zc1 = zero_crossings(&buf);
1183 eprintln!("zero crossings: stream0={zc0} stream1={zc1}");
1184 assert!((80..=96).contains(&zc0) && (168..=184).contains(&zc1), "stream order {zc0} {zc1}");
1185 assert!(open_audio(&p, 2).is_err());
1186 let t0 = Instant::now();
1187 let mut t = 0.0;
1188 for _ in 0..100 {
1189 a.read_at(t, &mut buf);
1190 t += 4800.0 / 48000.0;
1191 }
1192 eprintln!("100 sequential audio blocks (10 s): {:?}", t0.elapsed());
1193 }
1194
1195 #[test]
1197 fn steps_every_frame_mkv_and_mp4() {
1198 let Some(mkv) = derive("v30.mkv", &[], &["-c", "copy"]) else {
1199 eprintln!("ffmpeg missing; skipped");
1200 return;
1201 };
1202 for p in [mkv, media().unwrap()] {
1203 let mut v = open_video(&p).expect("open_video");
1204 let mut f = Frame::default();
1205 for n in 0..120 {
1206 let t = n as f64 / 30.0;
1207 assert!(v.frame_at(t, 32, 24, &mut f), "{p} frame {n}");
1208 assert!((f.pts - t).abs() < 0.002, "{p} frame {n}: pts {} (dup/skip)", f.pts);
1209 let c = px(&f, 1, 1);
1210 assert!(if n < 60 { is_red(c) } else { is_green(c) }, "{p} frame {n}: {c:?}");
1211 }
1212 }
1213 }
1214
1215 #[test]
1217 fn rotated_video_reports_display_size() {
1218 let Some(p) = derive("rot90.mp4", &["-display_rotation", "90"], &["-c", "copy"]) else {
1219 eprintln!("ffmpeg missing; skipped");
1220 return;
1221 };
1222 assert_eq!(probe(&p).map(|a| (a.width, a.height)), Ok((240, 320)), "mf probe");
1223 assert_eq!(crate::media::ffpipe::probe(&p).map(|a| (a.width, a.height)), Ok((240, 320)), "ffprobe");
1224 for mut v in [open_video(&p).expect("mf"), crate::media::ffpipe::open_video(&p).expect("ffpipe")] {
1225 assert_eq!(v.size(), (240, 320));
1226 let mut f = Frame::default();
1227 assert!(v.frame_at(0.5, 240, 320, &mut f));
1228 let (l, r) = (px(&f, 10, 160), px(&f, 230, 160));
1229 assert!((is_red(l) && is_blue(r)) || (is_blue(l) && is_red(r)), "halves {l:?} {r:?}");
1230 }
1231 }
1232
1233 #[test]
1237 fn offset_start_is_content_relative() {
1238 let (Some(ts), Some(mp4), Some(mkv)) = (
1239 derive("off.ts", &[], &["-c", "copy", "-f", "mpegts"]),
1240 derive("off10.mp4", &[], &["-c", "copy", "-output_ts_offset", "10"]),
1241 derive("off10.mkv", &[], &["-c", "copy", "-output_ts_offset", "10"]),
1242 ) else {
1243 eprintln!("ffmpeg missing; skipped");
1244 return;
1245 };
1246 assert!(open_video(&ts).is_err() && open_audio(&ts, 0).is_err(), "TS goes to ffmpeg");
1247 assert!(crate::media::open_video(&ts, crate::media::Backend::Auto).is_ok());
1248 for p in [mp4, mkv] {
1249 let mut v = open_video(&p).expect("open_video");
1250 let mut f = Frame::default();
1251 for (t, green) in [(0.5, false), (2.5, true), (0.5, false), (3.9, true), (0.0, false)] {
1252 assert!(v.frame_at(t, 32, 24, &mut f), "{p} frame_at({t})");
1253 let c = px(&f, 1, 1);
1254 assert!(if green { is_green(c) } else { is_red(c) }, "{p} t={t}: {c:?} pts {}", f.pts);
1255 assert!((f.pts - t).abs() < 0.05, "{p} t={t}: pts {}", f.pts);
1256 }
1257 assert!(!v.frame_at(4.5, 32, 24, &mut f), "{p} past end");
1258 let mut a = open_audio(&p, 0).expect("open_audio");
1259 let mut buf = vec![0f32; 4800 * 2];
1260 for t in [0.0, 1.0, 3.8, 0.5] {
1261 a.read_at(t, &mut buf);
1262 let r = rms(&buf);
1263 assert!((0.1..=1.0).contains(&r), "{p} audio t={t}: rms {r}");
1264 }
1265 }
1266 }
1267
1268 #[test]
1269 fn push_audio_converts() {
1270 let mut fifo = Vec::new();
1272 push_audio(&[1.0, 2.0, 3.0, 4.0], 2, 48000, &mut Resamp::default(), &mut fifo);
1273 assert_eq!(fifo, [1.0, 2.0, 3.0, 4.0]);
1274 fifo.clear();
1276 push_audio(&[0.5], 1, 48000, &mut Resamp::default(), &mut fifo);
1277 push_audio(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 6, 48000, &mut Resamp::default(), &mut fifo);
1278 assert_eq!(fifo, [0.5, 0.5, 1.0, 2.0]);
1279 fifo.clear();
1281 let mut rs = Resamp::default();
1282 push_audio(&[0.0, 2.0, 4.0], 1, 24000, &mut rs, &mut fifo);
1283 push_audio(&[6.0, 8.0], 1, 24000, &mut rs, &mut fifo);
1284 let l: Vec<f32> = fifo.chunks_exact(2).map(|c| c[0]).collect();
1285 assert_eq!(l, [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
1287 fifo.clear();
1289 push_audio(&[0.0, 1.0, 2.0, 3.0], 1, 96000, &mut Resamp::default(), &mut fifo);
1290 assert_eq!(fifo.len() / 2, 2);
1291 }
1292
1293 #[test]
1294 fn scalers() {
1295 let mut src = vec![0u8; 4 * 2 * 4];
1297 for (i, p) in src.chunks_exact_mut(4).enumerate() {
1298 p.copy_from_slice(if i % 4 < 2 { &[255, 0, 0, 255] } else { &[0, 0, 255, 255] });
1299 }
1300 let xs = [0u32, 2, 4];
1301 let ys = [0u32, 2];
1302 let mut acc = vec![0u32; 6];
1303 let mut dst = vec![0u8; 2 * 4];
1304 box_down(&src, 4, &xs, &ys, &mut acc, &mut dst);
1305 assert_eq!(dst, [255, 0, 0, 255, 0, 0, 255, 255]);
1306 let mut up = vec![0u8; 8 * 4 * 4];
1307 bilinear(&src, 4, 2, 8, 4, &mut up);
1308 assert_eq!(&up[..4], &[255, 0, 0, 255]);
1309 assert_eq!(&up[7 * 4..8 * 4], &[0, 0, 255, 255]);
1310 let rows: [[u8; 8]; 2] = [[1, 2, 3, 0, 4, 5, 6, 0], [7, 8, 9, 0, 10, 11, 12, 0]]; let mut out = vec![0u8; 16];
1313 unsafe { convert_rows(rows[1].as_ptr(), -8, 2, 2, &mut out) };
1314 assert_eq!(out, [9, 8, 7, 255, 12, 11, 10, 255, 3, 2, 1, 255, 6, 5, 4, 255]);
1315 }
1316}