1use crate::platform_time::Duration;
2use std::collections::{HashMap, HashSet, VecDeque};
3use std::fs;
4use std::io::{BufReader, Cursor, Read, Seek, SeekFrom};
5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
7use std::sync::{
8 Arc,
9 mpsc::{self, Receiver, TryRecvError},
10};
11use std::thread;
12
13use anyhow::{Context, Result, anyhow, bail};
14#[cfg(not(target_arch = "wasm32"))]
15use kira::Frame;
16use kira::sound::PlaybackState;
17use kira::sound::static_sound::{StaticSoundData, StaticSoundHandle};
18#[cfg(not(target_arch = "wasm32"))]
19use kira::sound::streaming::{
20 Decoder as KiraStreamingDecoder, StreamingSoundData, StreamingSoundHandle,
21};
22
23use crate::assets::RgbaImage;
24use crate::audio::{AudioHub, TrackKind};
25
26const MPEG2_HEADER_PROBE_BYTES: usize = 256 * 1024;
27const MPEG2_STREAM_CHUNK_BYTES: usize = 256 * 1024;
28const MPEG_AUDIO_HEAD_PROBE_MAX_BYTES: u64 = 16 * 1024 * 1024;
29const MPEG_AUDIO_TAIL_PROBE_INITIAL_BYTES: u64 = 4 * 1024 * 1024;
30const MPEG_AUDIO_TAIL_PROBE_MAX_BYTES: u64 = 32 * 1024 * 1024;
31const MPEG_AUDIO_SEEK_INITIAL_BACKTRACK_BYTES: u64 = 512 * 1024;
32const MPEG_AUDIO_SEEK_MAX_PRIME_BYTES: u64 = 16 * 1024 * 1024;
33const MPEG_VIDEO_SEEK_BACKTRACK_BYTES: u64 = 8 * 1024 * 1024;
34const MPEG_VIDEO_SEEK_FORWARD_PROBE_BYTES: u64 = 1024 * 1024;
35#[cfg(not(target_os = "vita"))]
36const MPEG2_STREAM_CHANNEL_CAPACITY: usize = 4;
37#[cfg(target_os = "vita")]
38const MPEG2_STREAM_CHANNEL_CAPACITY: usize = 1;
39const MPEG2_STREAM_MAX_DRAIN_EVENTS: usize = 8;
40#[cfg(not(target_os = "vita"))]
41const MPEG2_STREAM_FRAME_KEEP: usize = 6;
42#[cfg(target_os = "vita")]
43const MPEG2_STREAM_FRAME_KEEP: usize = 1;
44#[cfg(not(target_os = "vita"))]
45const MPEG2_STREAM_DECODE_LEAD_FRAMES: usize = 3;
46#[cfg(target_os = "vita")]
47const MPEG2_STREAM_DECODE_LEAD_FRAMES: usize = 1;
48#[cfg(not(any(target_os = "horizon", target_os = "vita")))]
52const OMV_STREAM_CHANNEL_CAPACITY: usize = 4;
53#[cfg(target_os = "horizon")]
54const OMV_STREAM_CHANNEL_CAPACITY: usize = 4;
55#[cfg(target_os = "vita")]
56const OMV_STREAM_CHANNEL_CAPACITY: usize = 1;
57#[cfg(not(any(target_os = "horizon", target_os = "vita")))]
58const OMV_STREAM_MAX_DRAIN_EVENTS: usize = 16;
59#[cfg(any(target_os = "horizon", target_os = "vita"))]
60const OMV_STREAM_MAX_DRAIN_EVENTS: usize = 8;
61#[cfg(not(any(target_os = "horizon", target_os = "vita")))]
62const OMV_STREAM_FRAME_KEEP: usize = 6;
63#[cfg(target_os = "horizon")]
64const OMV_STREAM_FRAME_KEEP: usize = 6;
65#[cfg(target_os = "vita")]
66const OMV_STREAM_FRAME_KEEP: usize = 1;
67#[cfg(not(any(target_os = "vita", feature = "virtual-clock")))]
68const OMV_STREAM_DECODE_LEAD_FRAMES: usize = 2;
69#[cfg(all(not(target_os = "vita"), feature = "virtual-clock"))]
72const OMV_STREAM_DECODE_LEAD_FRAMES: usize = 0;
73#[cfg(target_os = "vita")]
74const OMV_STREAM_DECODE_LEAD_FRAMES: usize = 1;
75const OMV_LOOP_HEAD_CACHE_MAX_FRAMES: usize = 4;
78#[cfg(not(target_os = "vita"))]
79const OMV_LOOP_HEAD_CACHE_MAX_BYTES: usize = 8 * 1024 * 1024;
80#[cfg(target_os = "vita")]
81const OMV_LOOP_HEAD_CACHE_MAX_BYTES: usize = 4 * 1024 * 1024;
82#[cfg(not(target_os = "vita"))]
83const WMV_STREAM_CHANNEL_CAPACITY: usize = 8;
84#[cfg(target_os = "vita")]
85const WMV_STREAM_CHANNEL_CAPACITY: usize = 1;
86const WMV_STREAM_MAX_DRAIN_EVENTS: usize = 16;
87#[cfg(not(target_os = "vita"))]
88const WMV_STREAM_FRAME_KEEP: usize = 12;
89#[cfg(target_os = "vita")]
90const WMV_STREAM_FRAME_KEEP: usize = 2;
91#[cfg(not(target_os = "vita"))]
92const WMV_STREAM_DECODE_LEAD_MS: usize = 750;
93#[cfg(target_os = "vita")]
94const WMV_STREAM_DECODE_LEAD_MS: usize = 150;
95
96fn lower_worker_thread_priority() {
101 #[cfg(target_os = "vita")]
102 unsafe {
103 unsafe extern "C" {
104 fn sceKernelGetThreadId() -> i32;
105 fn sceKernelGetThreadCurrentPriority() -> i32;
106 fn sceKernelChangeThreadPriority(thid: i32, priority: i32) -> i32;
107 }
108 let current = sceKernelGetThreadCurrentPriority();
109 if current > 0 {
110 sceKernelChangeThreadPriority(sceKernelGetThreadId(), (current + 16).min(191));
112 }
113 }
114}
115
116fn movie_output_dimensions(width: u32, height: u32) -> (u32, u32) {
117 #[cfg(target_os = "vita")]
118 {
119 const MAX_WIDTH: u32 = 960;
120 const MAX_HEIGHT: u32 = 544;
121 if width > MAX_WIDTH || height > MAX_HEIGHT {
122 if u64::from(width) * u64::from(MAX_HEIGHT) >= u64::from(height) * u64::from(MAX_WIDTH)
123 {
124 return (
125 MAX_WIDTH,
126 ((u64::from(height) * u64::from(MAX_WIDTH) / u64::from(width)) as u32).max(1),
127 );
128 }
129 return (
130 ((u64::from(width) * u64::from(MAX_HEIGHT) / u64::from(height)) as u32).max(1),
131 MAX_HEIGHT,
132 );
133 }
134 }
135 (width, height)
136}
137
138#[derive(Debug, Clone)]
139pub struct MovieInfo {
140 pub path: PathBuf,
141 pub width: Option<u32>,
142 pub height: Option<u32>,
143 pub fps: Option<f32>,
144 pub decoded_frames: Option<usize>,
145 pub audio_duration_ms: Option<u64>,
146}
147
148impl MovieInfo {
149 pub fn duration_ms(&self) -> Option<u64> {
150 if let Some(ms) = self.audio_duration_ms {
151 return Some(ms);
152 }
153 let fps = self.fps?;
154 let frames = self.decoded_frames?;
155 if fps <= 0.0 || frames == 0 {
156 return None;
157 }
158 Some(((frames as f64) * 1000.0 / (fps as f64)).round() as u64)
159 }
160}
161
162#[derive(Debug, Clone)]
163pub struct MovieStreamFrame {
164 pub frame: Arc<RgbaImage>,
165 pub frame_idx: usize,
166 pub fps: Option<f32>,
167 pub total_ms: Option<u64>,
168 pub audio: Option<MovieAudio>,
169 pub audio_ready: bool,
170 pub decoded_now: bool,
171 pub frame_idx_is_decode_order: bool,
175 pub clamped_timer_ms: Option<u64>,
176 pub source_size: Option<(u32, u32)>,
179}
180
181enum Mpeg2StreamEvent {
182 Info {
183 width: Option<u32>,
184 height: Option<u32>,
185 fps: Option<f32>,
186 },
187 Video {
188 frame_idx: usize,
189 pts_90k: Option<i64>,
190 frame: Arc<RgbaImage>,
191 },
192 Done,
193}
194
195#[derive(Clone)]
196struct Mpeg2DecodedFrame {
197 frame_idx: usize,
198 pts_90k: Option<i64>,
199 frame: Arc<RgbaImage>,
200}
201
202struct Mpeg2StreamState {
203 rx: Receiver<Result<Mpeg2StreamEvent, String>>,
204 frames: VecDeque<Mpeg2DecodedFrame>,
205 width: Option<u32>,
206 height: Option<u32>,
207 fps: Option<f32>,
208 decoded_frames: usize,
209 first_video_pts_90k: Option<i64>,
210 last_video_timeline_ms: Option<u64>,
211 last_requested_timer_ms: u64,
212 done: bool,
213 audio: Option<MovieAudio>,
214 decoded_any_this_poll: bool,
215 request_frames: Arc<AtomicUsize>,
216 #[cfg(target_os = "vita")]
218 idle_frames: u32,
219}
220
221impl Drop for Mpeg2StreamState {
222 fn drop(&mut self) {
223 self.request_frames.store(usize::MAX, Ordering::Release);
224 }
225}
226
227struct Mpeg2AudioTask {
228 rx: Receiver<Result<Option<MovieAudio>, String>>,
229 cancel: Arc<AtomicBool>,
230}
231
232impl Drop for Mpeg2AudioTask {
233 fn drop(&mut self) {
234 self.cancel.store(true, Ordering::Release);
235 }
236}
237
238enum WmvStreamEvent {
239 Info {
240 width: u32,
241 height: u32,
242 },
243 Video {
244 frame_idx: usize,
245 source_pts_ms: u64,
246 frame: Arc<RgbaImage>,
247 },
248 Done,
249}
250
251#[derive(Clone)]
252struct WmvDecodedFrame {
253 frame_idx: usize,
254 source_pts_ms: u64,
259 frame: Arc<RgbaImage>,
260}
261
262struct WmvStreamState {
263 rx: Receiver<Result<WmvStreamEvent, String>>,
264 frames: VecDeque<WmvDecodedFrame>,
265 width: Option<u32>,
266 height: Option<u32>,
267 fps: Option<f32>,
268 decoded_frames: usize,
269 timeline_origin_ms: Option<u64>,
270 last_video_source_pts_ms: Option<u64>,
271 last_frame_delta_ms: Option<u64>,
272 total_ms_hint: Option<u64>,
273 last_effective_timer_ms: u64,
274 done: bool,
275 audio: Option<MovieAudio>,
276 decoded_any_this_poll: bool,
277 request_ms: Arc<AtomicUsize>,
278 #[cfg(target_os = "vita")]
280 idle_frames: u32,
281}
282
283impl Drop for WmvStreamState {
284 fn drop(&mut self) {
285 self.request_ms.store(usize::MAX, Ordering::Release);
286 }
287}
288
289struct WmvAudioTask {
290 rx: Receiver<Result<Option<MovieAudio>, String>>,
291 cancel: Arc<AtomicBool>,
292}
293
294impl Drop for WmvAudioTask {
295 fn drop(&mut self) {
296 self.cancel.store(true, Ordering::Release);
297 }
298}
299
300enum OmvStreamEvent {
301 Info {
302 width: u32,
303 height: u32,
304 fps: Option<f32>,
305 frame_time_ms: Option<f64>,
306 total_frames_hint: Option<usize>,
307 total_ms_hint: Option<u64>,
308 frame_times: Arc<Vec<(u64, u64)>>,
309 },
310 Reset {
311 frame_idx: usize,
312 },
313 Video {
314 frame_idx: usize,
315 frame: Arc<RgbaImage>,
316 },
317 Done,
318}
319
320struct OmvStreamState {
321 rx: Receiver<Result<OmvStreamEvent, String>>,
322 frames: VecDeque<(usize, Arc<RgbaImage>)>,
323 loop_head_frames: VecDeque<(usize, Arc<RgbaImage>)>,
324 loop_head_bytes: usize,
325 width: Option<u32>,
326 height: Option<u32>,
327 fps: Option<f32>,
328 frame_time_ms: Option<f64>,
329 total_frames_hint: Option<usize>,
330 total_ms_hint: Option<u64>,
331 frame_times: Option<Arc<Vec<(u64, u64)>>>,
332 decoded_frames: usize,
333 done: bool,
334 request_frame: Arc<AtomicUsize>,
335 last_served_frame_idx: usize,
339 last_effective_timer_ms: u64,
342 held_frame: Option<(usize, Arc<RgbaImage>)>,
347 #[cfg(target_os = "vita")]
349 idle_frames: u32,
350}
351
352impl Drop for OmvStreamState {
353 fn drop(&mut self) {
354 self.request_frame.store(usize::MAX, Ordering::Release);
355 }
356}
357
358pub struct MovieManager {
366 project_dir: PathBuf,
367 current_append_dir: String,
368 current: Option<MovieInfo>,
369 cache: HashMap<PathBuf, MovieAsset>,
370 preview_cache: HashMap<PathBuf, Arc<RgbaImage>>,
371 decode_tasks: HashMap<PathBuf, Receiver<Result<MovieAsset, String>>>,
372 mpeg2_audio_cache: HashMap<PathBuf, Option<MovieAudio>>,
373 mpeg2_audio_tasks: HashMap<PathBuf, Mpeg2AudioTask>,
374 mpeg2_streams: HashMap<PathBuf, Mpeg2StreamState>,
375 wmv_audio_cache: HashMap<PathBuf, Option<MovieAudio>>,
376 wmv_audio_tasks: HashMap<PathBuf, WmvAudioTask>,
377 wmv_streams: HashMap<PathBuf, WmvStreamState>,
378 omv_streams: HashMap<PathBuf, OmvStreamState>,
379 playbacks: HashMap<u64, MoviePlayback>,
380 next_playback_id: u64,
381 path_cache: HashMap<(String, String), PathBuf>,
385}
386
387impl std::fmt::Debug for MovieManager {
388 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389 f.debug_struct("MovieManager")
390 .field("project_dir", &self.project_dir)
391 .field("current_append_dir", &self.current_append_dir)
392 .field("current", &self.current)
393 .field("cache_len", &self.cache.len())
394 .field("preview_cache_len", &self.preview_cache.len())
395 .field("decode_tasks_len", &self.decode_tasks.len())
396 .field("mpeg2_audio_cache_len", &self.mpeg2_audio_cache.len())
397 .field("mpeg2_audio_tasks_len", &self.mpeg2_audio_tasks.len())
398 .field("mpeg2_streams_len", &self.mpeg2_streams.len())
399 .field("wmv_audio_cache_len", &self.wmv_audio_cache.len())
400 .field("wmv_audio_tasks_len", &self.wmv_audio_tasks.len())
401 .field("wmv_streams_len", &self.wmv_streams.len())
402 .field("omv_streams_len", &self.omv_streams.len())
403 .field("playbacks_len", &self.playbacks.len())
404 .finish()
405 }
406}
407
408#[derive(Debug, Clone, Copy, Default)]
409pub struct MovieMemoryStats {
410 pub video_bytes: usize,
411 pub video_frames: usize,
412 pub audio_pcm_bytes: usize,
413 pub audio_buffers: usize,
414 pub asset_cache_entries: usize,
415 pub preview_cache_entries: usize,
416 pub active_streams: usize,
417}
418
419impl MovieManager {
420 #[cfg(target_os = "vita")]
429 pub fn evict_idle_streams(&mut self) {
430 const IDLE_FRAMES: u32 = 120;
431 let old_mpeg_count = self.mpeg2_streams.len();
432 let old_wmv_count = self.wmv_streams.len();
433 let keep = |idle: &mut u32| {
434 *idle = idle.saturating_add(1);
435 *idle <= IDLE_FRAMES
436 };
437 self.mpeg2_streams
438 .retain(|_, state| keep(&mut state.idle_frames));
439 self.wmv_streams
440 .retain(|_, state| keep(&mut state.idle_frames));
441 self.omv_streams
442 .retain(|_, state| keep(&mut state.idle_frames));
443 if self.mpeg2_streams.len() != old_mpeg_count {
444 self.mpeg2_audio_tasks
445 .retain(|path, _| self.mpeg2_streams.contains_key(path));
446 self.mpeg2_audio_cache
447 .retain(|path, _| self.mpeg2_streams.contains_key(path));
448 }
449 if self.wmv_streams.len() != old_wmv_count {
450 self.wmv_audio_tasks
451 .retain(|path, _| self.wmv_streams.contains_key(path));
452 self.wmv_audio_cache
453 .retain(|path, _| self.wmv_streams.contains_key(path));
454 }
455 }
456
457 pub fn debug_memory_stats(&self) -> MovieMemoryStats {
460 let mut stats = MovieMemoryStats {
461 asset_cache_entries: self.cache.len(),
462 preview_cache_entries: self.preview_cache.len(),
463 active_streams: self.mpeg2_streams.len()
464 + self.wmv_streams.len()
465 + self.omv_streams.len(),
466 ..MovieMemoryStats::default()
467 };
468 fn add_frame(
469 seen: &mut HashSet<usize>,
470 stats: &mut MovieMemoryStats,
471 frame: &Arc<RgbaImage>,
472 ) {
473 let key = Arc::as_ptr(frame) as usize;
474 if seen.insert(key) {
475 stats.video_bytes = stats.video_bytes.saturating_add(frame.rgba.len());
476 stats.video_frames += 1;
477 }
478 }
479 fn add_audio(seen: &mut HashSet<usize>, stats: &mut MovieMemoryStats, audio: &MovieAudio) {
480 let key = Arc::as_ptr(&audio.samples) as usize;
481 if seen.insert(key) {
482 stats.audio_pcm_bytes = stats.audio_pcm_bytes.saturating_add(
483 audio
484 .samples
485 .len()
486 .saturating_mul(std::mem::size_of::<i16>()),
487 );
488 stats.audio_buffers += 1;
489 }
490 }
491 let mut seen_frames = HashSet::new();
492 let mut seen_audio = HashSet::new();
493 for asset in self.cache.values() {
494 for frame in &asset.frames {
495 add_frame(&mut seen_frames, &mut stats, frame);
496 }
497 if let Some(audio) = asset.audio.as_ref() {
498 add_audio(&mut seen_audio, &mut stats, audio);
499 }
500 }
501 for frame in self.preview_cache.values() {
502 add_frame(&mut seen_frames, &mut stats, frame);
503 }
504 for state in self.mpeg2_streams.values() {
505 for frame in &state.frames {
506 add_frame(&mut seen_frames, &mut stats, &frame.frame);
507 }
508 if let Some(audio) = state.audio.as_ref() {
509 add_audio(&mut seen_audio, &mut stats, audio);
510 }
511 }
512 for state in self.wmv_streams.values() {
513 for frame in &state.frames {
514 add_frame(&mut seen_frames, &mut stats, &frame.frame);
515 }
516 if let Some(audio) = state.audio.as_ref() {
517 add_audio(&mut seen_audio, &mut stats, audio);
518 }
519 }
520 for state in self.omv_streams.values() {
521 for (_, frame) in &state.frames {
522 add_frame(&mut seen_frames, &mut stats, frame);
523 }
524 for (_, frame) in &state.loop_head_frames {
525 add_frame(&mut seen_frames, &mut stats, frame);
526 }
527 if let Some((_, frame)) = state.held_frame.as_ref() {
528 add_frame(&mut seen_frames, &mut stats, frame);
529 }
530 }
531 for audio in self.mpeg2_audio_cache.values().flatten() {
532 add_audio(&mut seen_audio, &mut stats, audio);
533 }
534 for audio in self.wmv_audio_cache.values().flatten() {
535 add_audio(&mut seen_audio, &mut stats, audio);
536 }
537 stats
538 }
539
540 pub fn new(project_dir: PathBuf) -> Self {
541 Self {
542 project_dir,
543 current_append_dir: String::new(),
544 current: None,
545 cache: HashMap::new(),
546 preview_cache: HashMap::new(),
547 decode_tasks: HashMap::new(),
548 mpeg2_audio_cache: HashMap::new(),
549 mpeg2_audio_tasks: HashMap::new(),
550 mpeg2_streams: HashMap::new(),
551 wmv_audio_cache: HashMap::new(),
552 wmv_audio_tasks: HashMap::new(),
553 wmv_streams: HashMap::new(),
554 omv_streams: HashMap::new(),
555 playbacks: HashMap::new(),
556 next_playback_id: 1,
557 path_cache: HashMap::new(),
558 }
559 }
560
561 pub fn current(&self) -> Option<&MovieInfo> {
562 self.current.as_ref()
563 }
564
565 pub fn set_current_append_dir(&mut self, append_dir: impl Into<String>) {
566 let append_dir = append_dir.into();
567 if self.current_append_dir != append_dir {
568 self.current_append_dir = append_dir;
569 }
570 }
571
572 pub fn set_current_append_dir_ref(&mut self, append_dir: &str) {
573 if self.current_append_dir != append_dir {
574 self.current_append_dir.clear();
575 self.current_append_dir.push_str(append_dir);
576 }
577 }
578
579 pub fn stop(&mut self) {
580 self.current = None;
581 self.mpeg2_streams.clear();
582 self.mpeg2_audio_tasks.clear();
583 self.wmv_streams.clear();
584 self.wmv_audio_tasks.clear();
585 self.omv_streams.clear();
586 #[cfg(target_os = "vita")]
587 {
588 self.cache.clear();
592 self.preview_cache.clear();
593 self.decode_tasks.clear();
594 self.mpeg2_audio_cache.clear();
595 self.wmv_audio_cache.clear();
596 }
597 }
598
599 pub fn prepare(&mut self, file_name: &str) -> Result<MovieInfo> {
600 self.play(file_name, false, false)
601 }
602
603 pub fn prepare_omv(&mut self, file_name: &str) -> Result<MovieInfo> {
604 let path = crate::resource::find_omv_path_with_append_dir(
605 &self.project_dir,
606 &self.current_append_dir,
607 file_name,
608 )?;
609 let header = read_omv_header_for_path(&path)
610 .with_context(|| format!("open OMV: {}", path.display()))?;
611 let w = header.display_width;
612 let h = header.display_height;
613 let fps = if header.frame_time_us != 0 {
614 Some(1_000_000.0 / (header.frame_time_us as f32))
615 } else {
616 None
617 };
618 let info = MovieInfo {
619 path,
620 width: (w > 0).then_some(w),
621 height: (h > 0).then_some(h),
622 fps,
623 decoded_frames: (header.packet_count_hint > 0)
624 .then_some(header.packet_count_hint as usize),
625 audio_duration_ms: None,
626 };
627 self.current = Some(info.clone());
628 Ok(info)
629 }
630
631 pub fn play(&mut self, file_name: &str, _wait: bool, _key_skip: bool) -> Result<MovieInfo> {
632 let path = resolve_mov_path(&self.project_dir, &self.current_append_dir, file_name)?;
633 let ext = path
634 .extension()
635 .and_then(|s| s.to_str())
636 .unwrap_or("")
637 .to_ascii_lowercase();
638
639 let info = if ext == "omv" {
640 let header = read_omv_header_for_path(&path)
641 .with_context(|| format!("open OMV: {}", path.display()))?;
642 let w = header.display_width;
643 let h = header.display_height;
644 let fps = if header.frame_time_us != 0 {
645 Some(1_000_000.0 / (header.frame_time_us as f32))
646 } else {
647 None
648 };
649 MovieInfo {
650 path,
651 width: (w > 0).then_some(w),
652 height: (h > 0).then_some(h),
653 fps,
654 decoded_frames: (header.packet_count_hint > 0)
655 .then_some(header.packet_count_hint as usize),
656 audio_duration_ms: None,
657 }
658 } else if is_wmv_extension(&ext) {
659 probe_wmv_movie_info(&path)?
660 } else {
661 let prefix = read_file_prefix(&path, MPEG2_HEADER_PROBE_BYTES)
662 .with_context(|| format!("read movie header: {}", path.display()))?;
663
664 let mut width = None;
665 let mut height = None;
666 let mut fps = None;
667
668 if let Some(h) = siglus_assets::mpeg2::find_sequence_header(&prefix) {
669 width = Some(h.width as u32);
670 height = Some(h.height as u32);
671 fps = siglus_assets::mpeg2::fps_from_frame_rate_code(h.frame_rate_code);
672 }
673
674 let decoded_frames = decode_frames_if_enabled(&path)?;
675
676 MovieInfo {
677 path,
678 width,
679 height,
680 fps,
681 decoded_frames,
682 audio_duration_ms: None,
683 }
684 };
685
686 self.current = Some(info.clone());
687 Ok(info)
688 }
689
690 pub fn ensure_asset(&mut self, file_name: &str) -> Result<(&MovieAsset, bool)> {
692 let path = resolve_mov_path(&self.project_dir, &self.current_append_dir, file_name)?;
693 self.ensure_asset_for_path(path)
694 }
695
696 pub fn ensure_omv_asset(&mut self, file_name: &str) -> Result<(&MovieAsset, bool)> {
697 let path = crate::resource::find_omv_path_with_append_dir(
698 &self.project_dir,
699 &self.current_append_dir,
700 file_name,
701 )?;
702 self.ensure_asset_for_path(path)
703 }
704
705 fn ensure_asset_for_path(&mut self, path: PathBuf) -> Result<(&MovieAsset, bool)> {
706 let existed = self.cache.contains_key(&path);
707 if !existed {
708 let asset = decode_asset_for_path(&path)?;
709 self.cache.insert(path.clone(), asset);
710 }
711 let asset = self.cache.get(&path).expect("asset cached");
712 Ok((asset, !existed))
713 }
714
715 pub fn poll_asset(&mut self, file_name: &str) -> Result<Option<(&MovieAsset, bool)>> {
716 let path = resolve_mov_path(&self.project_dir, &self.current_append_dir, file_name)?;
717 self.poll_asset_for_path(path)
718 }
719
720 pub fn poll_omv_asset(&mut self, file_name: &str) -> Result<Option<(&MovieAsset, bool)>> {
721 let path = crate::resource::find_omv_path_with_append_dir(
722 &self.project_dir,
723 &self.current_append_dir,
724 file_name,
725 )?;
726 self.poll_asset_for_path(path)
727 }
728
729 fn poll_asset_for_path(&mut self, path: PathBuf) -> Result<Option<(&MovieAsset, bool)>> {
730 if self.cache.contains_key(&path) {
731 let asset = self.cache.get(&path).expect("asset cached");
732 return Ok(Some((asset, false)));
733 }
734
735 let mut completed = None;
736 let mut failed = None;
737 if let Some(rx) = self.decode_tasks.get(&path) {
738 match rx.try_recv() {
739 Ok(Ok(asset)) => completed = Some(asset),
740 Ok(Err(err)) => failed = Some(err),
741 Err(TryRecvError::Empty) => {}
742 Err(TryRecvError::Disconnected) => {
743 failed = Some(format!(
744 "movie decode worker disconnected: {}",
745 path.display()
746 ));
747 }
748 }
749 } else {
750 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
751 {
752 let asset = decode_asset_for_path(&path)?;
753 self.cache.insert(path.clone(), asset);
754 let asset = self.cache.get(&path).expect("asset cached");
755 return Ok(Some((asset, true)));
756 }
757 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
758 {
759 let (tx, rx) = mpsc::channel();
760 let worker_path = path.clone();
761 thread::spawn(move || {
762 lower_worker_thread_priority();
763 let result =
764 decode_asset_for_path(&worker_path).map_err(|e| format!("{:#}", e));
765 let _ = tx.send(result);
766 });
767 self.decode_tasks.insert(path.clone(), rx);
768 }
769 }
770
771 if let Some(err) = failed {
772 self.decode_tasks.remove(&path);
773 return Err(anyhow!(err));
774 }
775 if let Some(asset) = completed {
776 self.decode_tasks.remove(&path);
777 self.cache.insert(path.clone(), asset);
778 let asset = self.cache.get(&path).expect("asset cached");
779 return Ok(Some((asset, true)));
780 }
781
782 Ok(None)
783 }
784
785 pub fn poll_global_movie_frame(
786 &mut self,
787 file_name: &str,
788 timer_ms: u64,
789 ) -> Result<Option<MovieStreamFrame>> {
790 self.poll_global_movie_frame_with_loop(file_name, timer_ms, false)
791 }
792
793 pub fn poll_global_movie_frame_with_loop(
794 &mut self,
795 file_name: &str,
796 timer_ms: u64,
797 loop_flag: bool,
798 ) -> Result<Option<MovieStreamFrame>> {
799 let path = self.resolve_mov_path_cached(file_name)?;
800 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
801 {
802 return self.poll_cached_movie_frame_for_path_with_loop(path, timer_ms, loop_flag);
803 }
804 let ext = path
805 .extension()
806 .and_then(|s| s.to_str())
807 .unwrap_or("")
808 .to_ascii_lowercase();
809 if ext == "omv" {
810 return self.poll_omv_stream_frame_for_path(path, timer_ms, loop_flag);
811 }
812 if is_wmv_extension(&ext) {
813 return self.poll_wmv_stream_frame_for_path(path, timer_ms, loop_flag);
814 }
815 self.poll_mpeg2_stream_frame_for_path(path, timer_ms)
816 }
817
818 fn resolve_mov_path_cached(&mut self, file_name: &str) -> Result<PathBuf> {
819 let key = (self.current_append_dir.clone(), file_name.to_string());
820 if let Some(path) = self.path_cache.get(&key) {
821 return Ok(path.clone());
822 }
823 let path = resolve_mov_path(&self.project_dir, &self.current_append_dir, file_name)?;
824 self.path_cache.insert(key, path.clone());
825 Ok(path)
826 }
827
828 fn poll_cached_movie_frame_for_path(
829 &mut self,
830 path: PathBuf,
831 timer_ms: u64,
832 ) -> Result<Option<MovieStreamFrame>> {
833 self.poll_cached_movie_frame_for_path_with_loop(path, timer_ms, false)
834 }
835
836 fn poll_cached_movie_frame_for_path_with_loop(
837 &mut self,
838 path: PathBuf,
839 timer_ms: u64,
840 loop_flag: bool,
841 ) -> Result<Option<MovieStreamFrame>> {
842 let (asset, decoded_now) = match self.poll_asset_for_path(path)? {
843 Some(v) => v,
844 None => return Ok(None),
845 };
846 if asset.frames.is_empty() {
847 return Ok(None);
848 }
849
850 let fps = asset.info.fps.unwrap_or_else(|| {
851 asset
852 .info
853 .duration_ms()
854 .filter(|ms| *ms > 0)
855 .map(|ms| (asset.frames.len() as f32) * 1000.0 / (ms as f32))
856 .unwrap_or(0.0)
857 });
858 let effective_timer_ms = if loop_flag {
859 asset
860 .info
861 .duration_ms()
862 .filter(|ms| *ms > 0)
863 .map(|ms| timer_ms % ms)
864 .unwrap_or(timer_ms)
865 } else {
866 timer_ms
867 };
868 let mut idx = frame_index_for_timer(effective_timer_ms, fps, asset.frames.len());
869 if idx >= asset.frames.len() {
870 idx = asset.frames.len() - 1;
871 }
872
873 Ok(Some(MovieStreamFrame {
874 frame: asset.frames[idx].clone(),
875 frame_idx: idx,
876 fps: (fps > 0.0).then_some(fps),
877 total_ms: asset.info.duration_ms(),
878 audio: asset.audio.clone(),
879 audio_ready: true,
880 decoded_now,
881 frame_idx_is_decode_order: false,
882 clamped_timer_ms: None,
883 source_size: asset.info.width.zip(asset.info.height),
884 }))
885 }
886
887 fn poll_mpeg2_stream_frame_for_path(
888 &mut self,
889 path: PathBuf,
890 timer_ms: u64,
891 ) -> Result<Option<MovieStreamFrame>> {
892 let (audio, audio_ready) = self.poll_mpeg2_audio_for_path(path.as_path());
893 if !self.mpeg2_streams.contains_key(&path) {
894 let state = spawn_mpeg2_stream_state(path.clone(), audio.clone(), timer_ms)?;
895 self.mpeg2_streams.insert(path.clone(), state);
896 } else if audio_ready && let Some(state) = self.mpeg2_streams.get_mut(&path) {
897 state.audio = audio.clone();
898 reindex_mpeg_stream_frames(state);
899 }
900
901 let desired_frame_idx = self.mpeg2_streams.get(&path).and_then(|state| {
902 state
903 .fps
904 .filter(|f| *f > 0.0)
905 .map(|fps| ((timer_ms as f64) * (fps as f64) / 1000.0).floor() as usize)
906 });
907 let restart_stream = self
908 .mpeg2_streams
909 .get(&path)
910 .is_some_and(|state| mpeg_stream_needs_restart(state, timer_ms));
911 if restart_stream {
912 self.mpeg2_streams.remove(&path);
913 let state = spawn_mpeg2_stream_state(path.clone(), audio.clone(), timer_ms)?;
914 self.mpeg2_streams.insert(path.clone(), state);
915 }
916
917 let state = self
918 .mpeg2_streams
919 .get_mut(&path)
920 .expect("mpeg2 stream state exists");
921 #[cfg(target_os = "vita")]
922 {
923 state.idle_frames = 0;
924 }
925 state.last_requested_timer_ms = timer_ms;
926 let request_until = desired_frame_idx
927 .unwrap_or(0)
928 .saturating_add(MPEG2_STREAM_DECODE_LEAD_FRAMES);
929 state.request_frames.store(request_until, Ordering::Release);
930 drain_mpeg2_stream_state(path.as_path(), state, desired_frame_idx, timer_ms)?;
931
932 if state.frames.is_empty() {
933 return Ok(None);
934 }
935
936 let Some(chosen) =
937 select_mpeg_stream_frame(&state.frames, state, timer_ms, desired_frame_idx).cloned()
938 else {
939 return Ok(None);
940 };
941
942 let video_total_ms = if state.done {
943 state.last_video_timeline_ms.map(|last| {
944 let frame_ms = state
945 .fps
946 .filter(|fps| *fps > 0.0)
947 .map(|fps| (1000.0 / fps as f64).round() as u64)
948 .unwrap_or(0);
949 last.saturating_add(frame_ms)
950 })
951 } else {
952 None
953 };
954 let audio_total_ms = state.audio.as_ref().map(|a| a.end_ms());
955 let total_ms = match (audio_total_ms, video_total_ms) {
956 (Some(a), Some(v)) => Some(a.max(v)),
957 (Some(a), None) => Some(a),
958 (None, Some(v)) => Some(v),
959 (None, None) => None,
960 };
961
962 let audio = state
963 .audio
964 .as_ref()
965 .filter(|track| timer_ms < track.end_ms())
966 .cloned();
967 let decoded_now = state.decoded_any_this_poll;
968 state.decoded_any_this_poll = false;
969
970 Ok(Some(MovieStreamFrame {
971 frame: chosen.frame,
972 frame_idx: chosen.frame_idx,
973 fps: state.fps,
974 total_ms,
975 audio,
976 audio_ready,
977 decoded_now,
978 frame_idx_is_decode_order: false,
979 clamped_timer_ms: None,
980 source_size: state.width.zip(state.height),
981 }))
982 }
983
984 fn poll_mpeg2_audio_for_path(&mut self, path: &Path) -> (Option<MovieAudio>, bool) {
985 if let Some(audio) = self.mpeg2_audio_cache.get(path) {
986 return (audio.clone(), true);
987 }
988
989 let mut completed = None;
990 let mut failed = None;
991 if let Some(task) = self.mpeg2_audio_tasks.get(path) {
992 match task.rx.try_recv() {
993 Ok(Ok(audio)) => completed = Some(audio),
994 Ok(Err(err)) => failed = Some(err),
995 Err(TryRecvError::Empty) => return (None, false),
996 Err(TryRecvError::Disconnected) => {
997 failed = Some(format!(
998 "mpeg2 audio worker disconnected: {}",
999 path.display()
1000 ));
1001 }
1002 }
1003 } else {
1004 let (tx, rx) = mpsc::channel();
1005 let cancel = Arc::new(AtomicBool::new(false));
1006 let worker_cancel = cancel.clone();
1007 let worker_path = path.to_path_buf();
1008 thread::spawn(move || {
1009 lower_worker_thread_priority();
1010 let result = decode_mpeg2_audio_for_path(&worker_path, worker_cancel.as_ref())
1011 .map_err(|err| format!("{:#}", err));
1012 let _ = tx.send(result);
1013 });
1014 self.mpeg2_audio_tasks
1015 .insert(path.to_path_buf(), Mpeg2AudioTask { rx, cancel });
1016 return (None, false);
1017 }
1018
1019 self.mpeg2_audio_tasks.remove(path);
1020 if let Some(err) = failed {
1021 eprintln!(
1022 "[SG_MOV] mpeg2 audio decode failed path={} err={}",
1023 path.display(),
1024 err
1025 );
1026 self.mpeg2_audio_cache.insert(path.to_path_buf(), None);
1027 return (None, true);
1028 }
1029 let audio = completed.unwrap_or(None);
1030 self.mpeg2_audio_cache
1031 .insert(path.to_path_buf(), audio.clone());
1032 (audio, true)
1033 }
1034
1035 fn poll_wmv_stream_frame_for_path(
1036 &mut self,
1037 path: PathBuf,
1038 timer_ms: u64,
1039 loop_flag: bool,
1040 ) -> Result<Option<MovieStreamFrame>> {
1041 let (audio, audio_ready) = self.poll_wmv_audio_for_path(path.as_path());
1042 if !self.wmv_streams.contains_key(&path) {
1043 let state = spawn_wmv_stream_state(path.clone(), audio.clone(), timer_ms)?;
1044 self.wmv_streams.insert(path.clone(), state);
1045 } else if audio_ready && let Some(state) = self.wmv_streams.get_mut(&path) {
1046 state.audio = audio.clone();
1047 }
1048
1049 let duration_hint = self.wmv_streams.get(&path).and_then(|state| {
1050 let video_total = state.total_ms_hint.or_else(|| {
1051 if state.done {
1052 match (state.timeline_origin_ms, state.last_video_source_pts_ms) {
1053 (Some(origin), Some(last)) => Some(
1054 last.saturating_sub(origin)
1055 .saturating_add(state.last_frame_delta_ms.unwrap_or(0)),
1056 ),
1057 _ => None,
1058 }
1059 } else {
1060 None
1061 }
1062 });
1063 let audio_total = state.audio.as_ref().map(MovieAudio::end_ms);
1064 match (video_total, audio_total) {
1065 (Some(v), Some(a)) => Some(v.max(a)),
1066 (Some(v), None) => Some(v),
1067 (None, Some(a)) => Some(a),
1068 (None, None) => None,
1069 }
1070 });
1071
1072 let effective_timer_ms = if loop_flag {
1078 duration_hint
1079 .filter(|duration| *duration > 0)
1080 .map(|duration| timer_ms % duration)
1081 .unwrap_or(timer_ms)
1082 } else {
1083 timer_ms
1084 };
1085
1086 let restart_stream = self
1087 .wmv_streams
1088 .get(&path)
1089 .map(|state| effective_timer_ms.saturating_add(100) < state.last_effective_timer_ms)
1090 .unwrap_or(false);
1091 if restart_stream {
1092 self.wmv_streams.remove(&path);
1093 let state = spawn_wmv_stream_state(path.clone(), audio.clone(), effective_timer_ms)?;
1094 self.wmv_streams.insert(path.clone(), state);
1095 }
1096
1097 let state = self
1098 .wmv_streams
1099 .get_mut(&path)
1100 .expect("wmv stream state exists");
1101 #[cfg(target_os = "vita")]
1102 {
1103 state.idle_frames = 0;
1104 }
1105 state.last_effective_timer_ms = effective_timer_ms;
1106 let request_ms = effective_timer_ms
1107 .saturating_add(WMV_STREAM_DECODE_LEAD_MS as u64)
1108 .min(usize::MAX as u64) as usize;
1109 state.request_ms.store(request_ms, Ordering::Release);
1110 drain_wmv_stream_state(path.as_path(), state, effective_timer_ms)?;
1111
1112 if state.timeline_origin_ms.is_none()
1117 && let Some(first) = state.frames.front()
1118 {
1119 let origin = first.source_pts_ms;
1120 state.timeline_origin_ms = Some(origin);
1121 state.total_ms_hint = state
1122 .total_ms_hint
1123 .and_then(|total| wmv_rebase_duration_ms(total, origin));
1124 }
1125 let Some(origin_ms) = state.timeline_origin_ms else {
1126 return Ok(None);
1127 };
1128 let presentation_pts_ms = origin_ms.saturating_add(effective_timer_ms);
1129
1130 discard_wmv_stream_frames(state, presentation_pts_ms);
1133
1134 let Some(chosen) = select_wmv_stream_frame(&state.frames, presentation_pts_ms).cloned()
1135 else {
1136 return Ok(None);
1137 };
1138
1139 let video_total_ms = state.total_ms_hint.or_else(|| {
1140 if state.done {
1141 state.last_video_source_pts_ms.map(|last| {
1142 last.saturating_sub(origin_ms)
1143 .saturating_add(state.last_frame_delta_ms.unwrap_or(0))
1144 })
1145 } else {
1146 None
1147 }
1148 });
1149 let audio_total_ms = state.audio.as_ref().map(MovieAudio::end_ms);
1150 let total_ms = match (audio_total_ms, video_total_ms) {
1151 (Some(a), Some(v)) => Some(a.max(v)),
1152 (Some(a), None) => Some(a),
1153 (None, Some(v)) => Some(v),
1154 (None, None) => None,
1155 };
1156 let audio = state
1157 .audio
1158 .as_ref()
1159 .filter(|track| loop_flag || effective_timer_ms < track.end_ms())
1160 .cloned();
1161 let decoded_now = state.decoded_any_this_poll;
1162 state.decoded_any_this_poll = false;
1163
1164 Ok(Some(MovieStreamFrame {
1165 frame: chosen.frame,
1166 frame_idx: chosen.frame_idx,
1167 fps: state.fps,
1168 total_ms,
1169 audio,
1170 audio_ready,
1171 decoded_now,
1172 frame_idx_is_decode_order: true,
1173 clamped_timer_ms: loop_flag.then_some(effective_timer_ms),
1174 source_size: state.width.zip(state.height),
1175 }))
1176 }
1177
1178 fn poll_wmv_audio_for_path(&mut self, path: &Path) -> (Option<MovieAudio>, bool) {
1179 if let Some(audio) = self.wmv_audio_cache.get(path) {
1180 return (audio.clone(), true);
1181 }
1182
1183 let mut completed = None;
1184 let mut failed = None;
1185 if let Some(task) = self.wmv_audio_tasks.get(path) {
1186 match task.rx.try_recv() {
1187 Ok(Ok(audio)) => completed = Some(audio),
1188 Ok(Err(err)) => failed = Some(err),
1189 Err(TryRecvError::Empty) => return (None, false),
1190 Err(TryRecvError::Disconnected) => {
1191 failed = Some(format!("wmv audio worker disconnected: {}", path.display()));
1192 }
1193 }
1194 } else {
1195 let (tx, rx) = mpsc::channel();
1196 let cancel = Arc::new(AtomicBool::new(false));
1197 let worker_cancel = cancel.clone();
1198 let worker_path = path.to_path_buf();
1199 thread::spawn(move || {
1200 lower_worker_thread_priority();
1201 let result = decode_wmv_audio_for_path(&worker_path, worker_cancel.as_ref())
1202 .map_err(|err| format!("{:#}", err));
1203 let _ = tx.send(result);
1204 });
1205 self.wmv_audio_tasks
1206 .insert(path.to_path_buf(), WmvAudioTask { rx, cancel });
1207 return (None, false);
1208 }
1209
1210 self.wmv_audio_tasks.remove(path);
1211 if let Some(err) = failed {
1212 eprintln!(
1213 "[SG_MOV] wmv audio decode failed path={} err={}",
1214 path.display(),
1215 err
1216 );
1217 self.wmv_audio_cache.insert(path.to_path_buf(), None);
1218 return (None, true);
1219 }
1220 let audio = completed.unwrap_or(None);
1221 self.wmv_audio_cache
1222 .insert(path.to_path_buf(), audio.clone());
1223 (audio, true)
1224 }
1225
1226 fn poll_omv_stream_frame_for_path(
1227 &mut self,
1228 path: PathBuf,
1229 timer_ms: u64,
1230 loop_flag: bool,
1231 ) -> Result<Option<MovieStreamFrame>> {
1232 if !self.omv_streams.contains_key(&path) {
1233 let state = spawn_omv_stream_state(path.clone())?;
1234 self.omv_streams.insert(path.clone(), state);
1235 }
1236 #[cfg(feature = "virtual-clock")]
1237 if let Some(state) = self.omv_streams.get_mut(&path) {
1238 settle_omv_stream(&path, state, None, |state| {
1239 state.fps.is_some() || state.frame_times.is_some()
1240 })?;
1241 }
1242
1243 let effective_timer_ms = self
1244 .omv_streams
1245 .get(&path)
1246 .and_then(|state| {
1247 if !loop_flag {
1248 return None;
1249 }
1250 state
1251 .total_ms_hint
1252 .or_else(|| {
1253 state.total_frames_hint.and_then(|frames| {
1254 state
1255 .frame_time_ms
1256 .filter(|ms| *ms > 0.0)
1257 .map(|ms| ((frames as f64) * ms).round() as u64)
1258 })
1259 })
1260 .filter(|duration| *duration > 0)
1261 })
1262 .map(|duration| timer_ms % duration)
1263 .unwrap_or(timer_ms);
1264 let desired_before_drain = self.omv_streams.get(&path).and_then(|state| {
1265 if let Some(frame_times) = state.frame_times.as_deref() {
1266 return omv_frame_for_time(frame_times, effective_timer_ms);
1267 }
1268 if let Some(ms) = state.frame_time_ms.filter(|v| *v > 0.0) {
1269 Some(((effective_timer_ms as f64) / ms).floor() as usize)
1270 } else {
1271 state.fps.filter(|f| *f > 0.0).map(|fps| {
1272 ((effective_timer_ms as f64) * (fps as f64) / 1000.0).floor() as usize
1273 })
1274 }
1275 });
1276
1277 let state = self
1278 .omv_streams
1279 .get_mut(&path)
1280 .expect("omv stream state exists");
1281 #[cfg(target_os = "vita")]
1282 {
1283 state.idle_frames = 0;
1284 }
1285 let timer_rewound = effective_timer_ms < state.last_effective_timer_ms.saturating_sub(200);
1286 state.last_effective_timer_ms = effective_timer_ms;
1287 let desired_before_drain = if timer_rewound {
1288 state.last_served_frame_idx = 0;
1291 desired_before_drain
1292 } else {
1293 desired_before_drain.map(|idx| idx.max(state.last_served_frame_idx))
1296 };
1297 let requested_frame = desired_before_drain.unwrap_or(0);
1298 state
1299 .request_frame
1300 .store(requested_frame, Ordering::Release);
1301 drain_omv_stream_state(
1302 path.as_path(),
1303 state,
1304 desired_before_drain,
1305 false,
1306 !cfg!(target_os = "vita"),
1307 )?;
1308 #[cfg(feature = "virtual-clock")]
1309 if let Some(desired) = desired_before_drain {
1310 settle_omv_stream(&path, state, desired_before_drain, |state| {
1311 state.frames.back().is_some_and(|(idx, _)| *idx >= desired)
1312 })?;
1313 }
1314
1315 let has_loop_head = loop_flag && !state.loop_head_frames.is_empty();
1316 if state.frames.is_empty() && !has_loop_head {
1317 return Ok(None);
1318 }
1319
1320 let latest_idx = state.decoded_frames.saturating_sub(1);
1321 let desired_idx = desired_before_drain.unwrap_or(latest_idx);
1322 let selected = if loop_flag || timer_rewound {
1323 select_omv_loop_frame(state, desired_idx)
1327 } else {
1328 select_stream_frame(&state.frames, desired_idx.min(latest_idx))
1329 };
1330 let Some((mut actual_frame_idx, mut frame)) = selected else {
1331 return Ok(None);
1332 };
1333 if !timer_rewound
1337 && actual_frame_idx < state.last_served_frame_idx
1338 && let Some((held_idx, held_frame)) = state.held_frame.clone()
1339 {
1340 actual_frame_idx = held_idx;
1341 frame = held_frame;
1342 }
1343 state.last_served_frame_idx = if timer_rewound {
1344 actual_frame_idx
1345 } else {
1346 state.last_served_frame_idx.max(actual_frame_idx)
1347 };
1348 state.held_frame = Some((actual_frame_idx, frame.clone()));
1349
1350 let video_total_ms = state.total_ms_hint.or_else(|| {
1351 if state.done && state.decoded_frames > 0 {
1352 state
1353 .frame_time_ms
1354 .map(|ms| ((state.decoded_frames as f64) * ms).round() as u64)
1355 .or_else(|| {
1356 state.fps.filter(|f| *f > 0.0).map(|fps| {
1357 ((state.decoded_frames as f64) * 1000.0 / (fps as f64)).round() as u64
1358 })
1359 })
1360 } else {
1361 state.total_frames_hint.and_then(|frames| {
1362 state
1363 .frame_time_ms
1364 .map(|ms| ((frames as f64) * ms).round() as u64)
1365 .or_else(|| {
1366 state
1367 .fps
1368 .filter(|f| *f > 0.0)
1369 .map(|fps| ((frames as f64) * 1000.0 / (fps as f64)).round() as u64)
1370 })
1371 })
1372 }
1373 });
1374
1375 Ok(Some(MovieStreamFrame {
1376 frame,
1377 frame_idx: actual_frame_idx,
1378 fps: state.fps,
1379 total_ms: video_total_ms,
1380 audio: None,
1381 audio_ready: true,
1382 decoded_now: false,
1383 frame_idx_is_decode_order: false,
1384 clamped_timer_ms: None,
1385 source_size: state.width.zip(state.height),
1386 }))
1387 }
1388
1389 pub fn ensure_preview_frame(&mut self, file_name: &str) -> Result<Arc<RgbaImage>> {
1390 let path = resolve_mov_path(&self.project_dir, &self.current_append_dir, file_name)?;
1391 self.ensure_preview_frame_for_path(path)
1392 }
1393
1394 pub fn ensure_omv_preview_frame(&mut self, file_name: &str) -> Result<Arc<RgbaImage>> {
1395 let path = crate::resource::find_omv_path_with_append_dir(
1396 &self.project_dir,
1397 &self.current_append_dir,
1398 file_name,
1399 )?;
1400 self.ensure_preview_frame_for_path(path)
1401 }
1402
1403 fn ensure_preview_frame_for_path(&mut self, path: PathBuf) -> Result<Arc<RgbaImage>> {
1404 if let Some(frame) = self.preview_cache.get(&path) {
1405 return Ok(frame.clone());
1406 }
1407 let ext = path
1408 .extension()
1409 .and_then(|s| s.to_str())
1410 .unwrap_or("")
1411 .to_ascii_lowercase();
1412 let frame = if ext == "omv" {
1413 decode_omv_preview_frame(&path)?
1414 } else if is_wmv_extension(&ext) {
1415 decode_wmv_preview_frame(&path)?
1416 } else {
1417 decode_mpeg2_preview_frame(&path)?
1418 };
1419 self.preview_cache.insert(path, frame.clone());
1420 Ok(frame)
1421 }
1422 pub fn start_audio(
1423 &mut self,
1424 audio: &mut AudioHub,
1425 track: &MovieAudio,
1426 offset_ms: u64,
1427 loop_flag: bool,
1428 ) -> Result<u64> {
1429 #[cfg(target_os = "horizon")]
1430 crate::switch_host::report_switch_marker(b"siglus_switch: movie audio start begin\n\0");
1431 let local_offset_ms = offset_ms.saturating_sub(track.start_ms);
1432
1433 #[cfg(not(target_arch = "wasm32"))]
1438 let (handle, timeline_base_ms) = if let Some(stream) = track.mpeg_stream.as_ref() {
1439 let decoder = MpegMovieAudioDecoder::new(stream.clone(), track.sample_rate)?;
1440 let mut data = StreamingSoundData::from_decoder(decoder)
1441 .start_position(local_offset_ms as f64 / 1000.0);
1442 if loop_flag {
1443 data = data.loop_region(..);
1444 }
1445 (
1446 MoviePlaybackHandle::Streaming(audio.play_streaming(TrackKind::Mov, data)?),
1447 0,
1448 )
1449 } else if let Some(stream) = track.wmv_stream.as_ref() {
1450 let mut decoder = WmvMovieAudioDecoder::new(stream.clone(), track.sample_rate)?;
1451
1452 let timeline_base_ms = if !loop_flag && local_offset_ms > 0 {
1462 let requested_frames = (((local_offset_ms as u128) * track.sample_rate as u128)
1463 / 1000)
1464 .min(stream.num_frames.saturating_sub(1) as u128)
1465 as usize;
1466 let actual_frames = decoder.prime_to_timeline_frame(requested_frames)?;
1467 track.start_ms.saturating_add(
1468 ((actual_frames as u128) * 1000 / track.sample_rate as u128)
1469 .min(u64::MAX as u128) as u64,
1470 )
1471 } else {
1472 track.start_ms
1473 };
1474
1475 let mut data = StreamingSoundData::from_decoder(decoder);
1476 if loop_flag {
1477 data = data
1480 .start_position(local_offset_ms as f64 / 1000.0)
1481 .loop_region(..);
1482 }
1483 (
1484 MoviePlaybackHandle::Streaming(audio.play_streaming(TrackKind::Mov, data)?),
1485 timeline_base_ms,
1486 )
1487 } else {
1488 make_static_movie_playback(audio, track, local_offset_ms, loop_flag)?
1489 };
1490
1491 #[cfg(target_arch = "wasm32")]
1492 let (handle, timeline_base_ms) =
1493 make_static_movie_playback(audio, track, local_offset_ms, loop_flag)?;
1494
1495 let id = self.next_playback_id;
1496 self.next_playback_id = self.next_playback_id.saturating_add(1).max(1);
1497 self.playbacks.insert(
1498 id,
1499 MoviePlayback {
1500 handle,
1501 timeline_base_ms,
1502 },
1503 );
1504 #[cfg(target_os = "horizon")]
1505 crate::switch_host::report_switch_marker(b"siglus_switch: movie audio start complete\n\0");
1506 Ok(id)
1507 }
1508
1509 pub fn pause_audio(&mut self, id: u64) {
1510 if let Some(p) = self.playbacks.get_mut(&id) {
1511 p.pause();
1512 }
1513 }
1514
1515 pub fn resume_audio(&mut self, id: u64) {
1516 if let Some(p) = self.playbacks.get_mut(&id) {
1517 p.resume();
1518 }
1519 }
1520
1521 pub fn stop_audio(&mut self, id: u64) {
1522 if let Some(mut p) = self.playbacks.remove(&id) {
1523 p.stop();
1524 }
1525 }
1526
1527 pub fn audio_playback_position_ms(&mut self, id: u64) -> Option<u64> {
1528 let (position_ms, stream_error) = {
1529 let p = self.playbacks.get_mut(&id)?;
1530 (p.movie_position_ms(), p.take_stream_error())
1531 };
1532 if let Some(err) = stream_error {
1533 eprintln!("[SG_MOV] streaming audio decoder error: {err:#}");
1534 if let Some(mut p) = self.playbacks.remove(&id) {
1535 p.stop();
1536 }
1537 return None;
1538 }
1539 Some(position_ms)
1540 }
1541
1542 pub fn audio_playback_finished(&mut self, id: u64) -> bool {
1543 let (finished, stream_error) = {
1544 let Some(p) = self.playbacks.get_mut(&id) else {
1545 return false;
1546 };
1547 (p.state() == PlaybackState::Stopped, p.take_stream_error())
1548 };
1549 if let Some(err) = stream_error {
1550 eprintln!("[SG_MOV] streaming audio decoder error: {err:#}");
1554 if let Some(mut p) = self.playbacks.remove(&id) {
1555 p.stop();
1556 }
1557 return false;
1558 }
1559 if finished {
1560 self.playbacks.remove(&id);
1561 }
1562 finished
1563 }
1564}
1565
1566fn resolve_mov_path(
1567 project_dir: &Path,
1568 current_append_dir: &str,
1569 file_name: &str,
1570) -> Result<PathBuf> {
1571 let (path, _ty) =
1572 crate::resource::find_mov_path_with_append_dir(project_dir, current_append_dir, file_name)?;
1573 Ok(path)
1574}
1575
1576fn decode_frames_if_enabled(_path: &Path) -> Result<Option<usize>> {
1577 Ok(None)
1578}
1579
1580fn read_file_prefix(path: &Path, max_len: usize) -> Result<Vec<u8>> {
1581 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1582 {
1583 let mut out = read_movie_bytes(path)?;
1584 out.truncate(max_len.min(out.len()));
1585 return Ok(out);
1586 }
1587 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1588 {
1589 let mut file =
1590 fs::File::open(path).with_context(|| format!("open file: {}", path.display()))?;
1591 let mut out = vec![0u8; max_len.max(1)];
1592 let n = file
1593 .read(&mut out)
1594 .with_context(|| format!("read file prefix: {}", path.display()))?;
1595 out.truncate(n);
1596 Ok(out)
1597 }
1598}
1599
1600fn frame_index_for_timer(timer_ms: u64, fps: f32, frame_count: usize) -> usize {
1601 if frame_count == 0 {
1602 return 0;
1603 }
1604 if fps <= 0.0 {
1605 return 0;
1606 }
1607 ((timer_ms as f64) * (fps as f64) / 1000.0).floor() as usize
1608}
1609
1610fn mpeg_stream_needs_restart(state: &Mpeg2StreamState, timer_ms: u64) -> bool {
1617 let rewound = timer_ms.saturating_add(40) < state.last_requested_timer_ms;
1618 let jumped_forward = timer_ms > state.last_requested_timer_ms.saturating_add(5_000);
1619 let before_cache = state
1620 .frames
1621 .front()
1622 .and_then(|frame| mpeg_frame_timeline_ms(frame, state))
1623 .is_some_and(|front| timer_ms.saturating_add(40) < front);
1624 let after_cache = state
1625 .frames
1626 .back()
1627 .and_then(|frame| mpeg_frame_timeline_ms(frame, state))
1628 .is_some_and(|back| timer_ms > back.saturating_add(5_000));
1629 (rewound && (before_cache || state.frames.is_empty())) || (jumped_forward && after_cache)
1630}
1631
1632fn spawn_mpeg2_stream_state(
1633 path: PathBuf,
1634 audio: Option<MovieAudio>,
1635 target_ms: u64,
1636) -> Result<Mpeg2StreamState> {
1637 let prefix = read_file_prefix(&path, MPEG2_HEADER_PROBE_BYTES)?;
1638 let mut width = None;
1639 let mut height = None;
1640 let mut fps = None;
1641 if let Some(h) = siglus_assets::mpeg2::find_sequence_header(&prefix) {
1642 width = Some(h.width as u32);
1643 height = Some(h.height as u32);
1644 fps = siglus_assets::mpeg2::fps_from_frame_rate_code(h.frame_rate_code);
1645 }
1646
1647 let first_video_pts_90k = audio
1648 .as_ref()
1649 .and_then(|track| track.mpeg_stream.as_ref())
1650 .and_then(|stream| stream.first_video_pts_90k)
1651 .or_else(|| probe_first_video_pts_90k(&prefix));
1652 let start_offset = estimate_mpeg_video_seek_offset(&path, target_ms, audio.as_ref())?;
1653 let requested_frame_idx = fps
1654 .filter(|value| *value > 0.0)
1655 .map(|value| ((target_ms as f64) * value as f64 / 1000.0).floor() as usize)
1656 .unwrap_or(0);
1657 let initial_frame_idx = 0usize;
1660 let (tx, rx) = mpsc::sync_channel(MPEG2_STREAM_CHANNEL_CAPACITY);
1661 let request_frames = Arc::new(AtomicUsize::new(
1662 requested_frame_idx.saturating_add(MPEG2_STREAM_DECODE_LEAD_FRAMES),
1663 ));
1664 let worker_request_frames = request_frames.clone();
1665 let video_path = path.clone();
1666 thread::spawn(move || {
1667 lower_worker_thread_priority();
1668 let result = stream_mpeg2_video_worker(
1669 video_path.as_path(),
1670 tx.clone(),
1671 worker_request_frames,
1672 start_offset,
1673 initial_frame_idx,
1674 first_video_pts_90k,
1675 );
1676 if let Err(err) = result {
1677 let _ = tx.send(Err(format!("{:#}", err)));
1678 }
1679 });
1680
1681 Ok(Mpeg2StreamState {
1682 rx,
1683 frames: VecDeque::new(),
1684 width,
1685 height,
1686 fps,
1687 decoded_frames: 0,
1688 first_video_pts_90k,
1689 last_video_timeline_ms: None,
1690 last_requested_timer_ms: target_ms,
1691 done: false,
1692 audio,
1693 decoded_any_this_poll: false,
1694 request_frames,
1695 #[cfg(target_os = "vita")]
1696 idle_frames: 0,
1697 })
1698}
1699
1700fn probe_first_video_pts_90k(data: &[u8]) -> Option<i64> {
1701 let mut demux = na_mpeg2_decoder::Demuxer::new_auto();
1702 demux.push(data, None).into_iter().find_map(|packet| {
1703 (packet.stream_type == na_mpeg2_decoder::StreamType::MpegVideo)
1704 .then_some(packet.pts_90k)
1705 .flatten()
1706 })
1707}
1708
1709fn estimate_mpeg_video_seek_offset(
1710 path: &Path,
1711 target_ms: u64,
1712 audio: Option<&MovieAudio>,
1713) -> Result<u64> {
1714 if target_ms < 2_000 {
1715 return Ok(0);
1716 }
1717 let file_len = fs::metadata(path)
1718 .with_context(|| format!("stat movie file: {}", path.display()))?
1719 .len();
1720 let duration_ms = audio
1724 .and_then(|track| track.duration_ms)
1725 .or_else(|| probe_mpeg_video_duration_ms(path))
1726 .unwrap_or(0);
1727
1728 if file_len <= MPEG2_STREAM_CHUNK_BYTES as u64 || duration_ms == 0 {
1729 return Ok(0);
1730 }
1731 let proportional = ((file_len as u128).saturating_mul(target_ms.min(duration_ms) as u128)
1732 / duration_ms as u128)
1733 .min(file_len as u128) as u64;
1734 let transport = audio
1735 .and_then(|track| track.mpeg_stream.as_ref())
1736 .map(|stream| stream.transport_stream)
1737 .unwrap_or_else(|| {
1738 read_file_prefix(path, 188 * 3)
1739 .map(|prefix| is_transport_stream_prefix(&prefix))
1740 .unwrap_or(false)
1741 });
1742
1743 let mut region_start =
1744 proportional.saturating_sub(MPEG_VIDEO_SEEK_BACKTRACK_BYTES.min(proportional));
1745 if transport {
1746 region_start -= region_start % 188;
1747 }
1748 let region_end = proportional
1749 .saturating_add(MPEG_VIDEO_SEEK_FORWARD_PROBE_BYTES)
1750 .min(file_len);
1751 let mut file = fs::File::open(path)
1752 .with_context(|| format!("open movie seek probe: {}", path.display()))?;
1753 file.seek(SeekFrom::Start(region_start))
1754 .with_context(|| format!("seek movie seek probe: {}", path.display()))?;
1755 let mut probe = vec![0u8; region_end.saturating_sub(region_start) as usize];
1756 let n = file
1757 .read(&mut probe)
1758 .with_context(|| format!("read movie seek probe: {}", path.display()))?;
1759 probe.truncate(n);
1760 if probe.is_empty() {
1761 return Ok(region_start);
1762 }
1763
1764 let target_in_probe = proportional.saturating_sub(region_start) as usize;
1765 let sequence_pos =
1769 rfind_byte_pattern_before(&probe, b"\0\0\x01\xb3", target_in_probe.min(probe.len()))
1770 .or_else(|| {
1771 rfind_byte_pattern_before(&probe, b"\0\0\x01\xb8", target_in_probe.min(probe.len()))
1772 });
1773 let Some(sequence_pos) = sequence_pos else {
1774 return Ok(0);
1775 };
1776
1777 if transport {
1778 let packet = sequence_pos / 188;
1779 for packet_index in (0..=packet).rev() {
1780 let pos = packet_index * 188;
1781 if pos + 188 > probe.len() || probe[pos] != 0x47 || (probe[pos + 1] & 0x40) == 0 {
1782 continue;
1783 }
1784 let afc = (probe[pos + 3] >> 4) & 0x3;
1785 let mut payload = pos + 4;
1786 if afc == 2 || afc == 3 {
1787 if payload >= pos + 188 {
1788 continue;
1789 }
1790 payload = payload.saturating_add(1 + probe[payload] as usize);
1791 }
1792 if payload + 4 <= pos + 188
1793 && probe[payload] == 0
1794 && probe[payload + 1] == 0
1795 && probe[payload + 2] == 1
1796 && (0xE0..=0xEF).contains(&probe[payload + 3])
1797 {
1798 return Ok(region_start.saturating_add(pos as u64));
1799 }
1800 }
1801 return Ok(region_start.saturating_add((packet * 188) as u64));
1802 }
1803
1804 let pack_pos =
1805 rfind_byte_pattern_before(&probe, b"\0\0\x01\xba", sequence_pos).unwrap_or(sequence_pos);
1806 Ok(region_start.saturating_add(pack_pos as u64))
1807}
1808
1809fn mpeg_sequence_header_es(head: &[u8]) -> Option<Vec<u8>> {
1812 let mut demux = na_mpeg2_decoder::Demuxer::new_auto();
1813 let es: Vec<u8> = demux
1814 .push(head, None)
1815 .into_iter()
1816 .filter(|packet| packet.stream_type == na_mpeg2_decoder::StreamType::MpegVideo)
1817 .flat_map(|packet| packet.data.to_vec())
1818 .collect();
1819 let start = es.windows(4).position(|w| w == b"\0\0\x01\xb3")?;
1820 let end = es[start + 4..]
1821 .windows(4)
1822 .position(|w| w[..3] == [0, 0, 1] && (w[3] == 0xb8 || w[3] == 0x00))
1823 .map(|at| start + 4 + at)?;
1824 Some(es[start..end].to_vec())
1825}
1826
1827fn probe_mpeg_video_duration_ms(path: &Path) -> Option<u64> {
1830 const TAIL_BYTES: u64 = 1024 * 1024;
1831 let head = read_file_prefix(path, MPEG2_HEADER_PROBE_BYTES).ok()?;
1832 let first = probe_first_video_pts_90k(&head)?;
1833 let mut file = fs::File::open(path).ok()?;
1834 let len = file.metadata().ok()?.len();
1835 let start = len.saturating_sub(TAIL_BYTES);
1836 file.seek(SeekFrom::Start(start)).ok()?;
1837 let mut tail = Vec::new();
1838 file.read_to_end(&mut tail).ok()?;
1839 let sync = if is_transport_stream_prefix(&head) {
1841 tail.iter().position(|&b| b == 0x47).unwrap_or(0)
1842 } else {
1843 tail.windows(4)
1844 .position(|w| w == b"\0\0\x01\xba")
1845 .unwrap_or(0)
1846 };
1847 let mut demux = na_mpeg2_decoder::Demuxer::new_auto();
1848 let last = demux
1849 .push(&tail[sync..], None)
1850 .into_iter()
1851 .filter(|packet| packet.stream_type == na_mpeg2_decoder::StreamType::MpegVideo)
1852 .filter_map(|packet| packet.pts_90k)
1853 .max()?;
1854 (last > first).then(|| ((last - first) / 90) as u64)
1855}
1856
1857fn rfind_byte_pattern_before(haystack: &[u8], needle: &[u8], end: usize) -> Option<usize> {
1858 if needle.is_empty() || haystack.len() < needle.len() {
1859 return None;
1860 }
1861 let end = end.min(haystack.len());
1862 if end < needle.len() {
1863 return None;
1864 }
1865 haystack[..end]
1866 .windows(needle.len())
1867 .rposition(|window| window == needle)
1868}
1869
1870fn wait_for_mpeg2_frame_request(request_frames: &Arc<AtomicUsize>, frame_idx: usize) {
1871 while frame_idx > request_frames.load(Ordering::Acquire) {
1872 if request_frames.load(Ordering::Acquire) == usize::MAX {
1873 return;
1874 }
1875 thread::sleep(Duration::from_millis(1));
1876 }
1877}
1878
1879fn stream_mpeg2_video_worker(
1880 path: &Path,
1881 tx: mpsc::SyncSender<Result<Mpeg2StreamEvent, String>>,
1882 request_frames: Arc<AtomicUsize>,
1883 start_offset: u64,
1884 initial_frame_idx: usize,
1885 #[allow(unused_variables)] video_origin_pts_90k: Option<i64>,
1886) -> Result<()> {
1887 let prefix = read_file_prefix(path, MPEG2_HEADER_PROBE_BYTES)?;
1888 let mut width = None;
1889 let mut height = None;
1890 let mut fps = None;
1891 if let Some(h) = siglus_assets::mpeg2::find_sequence_header(&prefix) {
1892 width = Some(h.width as u32);
1893 height = Some(h.height as u32);
1894 fps = siglus_assets::mpeg2::fps_from_frame_rate_code(h.frame_rate_code);
1895 }
1896 if tx
1897 .send(Ok(Mpeg2StreamEvent::Info { width, height, fps }))
1898 .is_err()
1899 {
1900 return Ok(());
1901 }
1902
1903 let mut file =
1904 fs::File::open(path).with_context(|| format!("open movie file: {}", path.display()))?;
1905 if start_offset > 0 {
1906 file.seek(SeekFrom::Start(start_offset))
1907 .with_context(|| format!("seek movie video: {}", path.display()))?;
1908 }
1909 let mut pipeline = na_mpeg2_decoder::MpegVideoPipeline::new();
1910 if start_offset > 0
1911 && let Some(header) = mpeg_sequence_header_es(&prefix)
1912 {
1913 let _ = pipeline.decoder_mut().decode_shared(&header, None);
1916 }
1917 let mut buf = vec![0u8; MPEG2_STREAM_CHUNK_BYTES];
1918 let mut frame_idx = initial_frame_idx;
1919 let mut send_failed = false;
1920 #[cfg(target_os = "vita")]
1921 let mut sent_video = false;
1922
1923 loop {
1924 let n = file
1925 .read(&mut buf)
1926 .with_context(|| format!("read movie stream: {}", path.display()))?;
1927 if n == 0 {
1928 break;
1929 }
1930 pipeline
1931 .push_with(&buf[..n], None, |f| {
1932 if send_failed {
1933 return;
1934 }
1935 wait_for_mpeg2_frame_request(&request_frames, frame_idx);
1936 if request_frames.load(Ordering::Acquire) == usize::MAX {
1937 send_failed = true;
1938 return;
1939 }
1940 #[cfg(target_os = "vita")]
1941 if sent_video
1942 && vita_mpeg_frame_is_late(
1943 f.pts_90k,
1944 video_origin_pts_90k,
1945 fps,
1946 request_frames.load(Ordering::Acquire),
1947 )
1948 {
1949 frame_idx = frame_idx.saturating_add(1);
1950 return;
1951 }
1952 let w = f.width as u32;
1953 let h = f.height as u32;
1954 let (w, h) = movie_output_dimensions(w, h);
1955 let mut rgba = vec![0u8; (w as usize).saturating_mul(h as usize).saturating_mul(4)];
1956 na_mpeg2_decoder::frame_to_rgba_bt601_limited_scaled(
1957 &f, &mut rgba, w as usize, h as usize,
1958 );
1959 let frame = Arc::new(RgbaImage {
1960 width: w,
1961 height: h,
1962 center_x: 0,
1963 center_y: 0,
1964 rgba,
1965 });
1966 let ev = Mpeg2StreamEvent::Video {
1967 frame_idx,
1968 pts_90k: f.pts_90k,
1969 frame,
1970 };
1971 frame_idx = frame_idx.saturating_add(1);
1972 if tx.send(Ok(ev)).is_err() {
1973 send_failed = true;
1974 } else {
1975 #[cfg(target_os = "vita")]
1976 {
1977 sent_video = true;
1978 }
1979 }
1980 })
1981 .context("mpeg2 stream video decode")?;
1982 if send_failed {
1983 return Ok(());
1984 }
1985 }
1986
1987 pipeline.flush_with(|f| {
1988 if send_failed {
1989 return;
1990 }
1991 wait_for_mpeg2_frame_request(&request_frames, frame_idx);
1992 if request_frames.load(Ordering::Acquire) == usize::MAX {
1993 send_failed = true;
1994 return;
1995 }
1996 #[cfg(target_os = "vita")]
1997 if sent_video
1998 && vita_mpeg_frame_is_late(
1999 f.pts_90k,
2000 video_origin_pts_90k,
2001 fps,
2002 request_frames.load(Ordering::Acquire),
2003 )
2004 {
2005 frame_idx = frame_idx.saturating_add(1);
2006 return;
2007 }
2008 let w = f.width as u32;
2009 let h = f.height as u32;
2010 let (w, h) = movie_output_dimensions(w, h);
2011 let mut rgba = vec![0u8; (w as usize).saturating_mul(h as usize).saturating_mul(4)];
2012 na_mpeg2_decoder::frame_to_rgba_bt601_limited_scaled(&f, &mut rgba, w as usize, h as usize);
2013 let frame = Arc::new(RgbaImage {
2014 width: w,
2015 height: h,
2016 center_x: 0,
2017 center_y: 0,
2018 rgba,
2019 });
2020 let ev = Mpeg2StreamEvent::Video {
2021 frame_idx,
2022 pts_90k: f.pts_90k,
2023 frame,
2024 };
2025 frame_idx = frame_idx.saturating_add(1);
2026 if tx.send(Ok(ev)).is_err() {
2027 send_failed = true;
2028 }
2029 })?;
2030
2031 if !send_failed {
2032 let _ = tx.send(Ok(Mpeg2StreamEvent::Done));
2033 }
2034 Ok(())
2035}
2036
2037#[cfg(target_os = "vita")]
2038fn vita_mpeg_frame_is_late(
2039 pts_90k: Option<i64>,
2040 origin_90k: Option<i64>,
2041 fps: Option<f32>,
2042 request_until: usize,
2043) -> bool {
2044 let (Some(pts), Some(origin), Some(fps)) = (pts_90k, origin_90k, fps) else {
2045 return false;
2046 };
2047 if fps <= 0.0 || request_until == usize::MAX {
2048 return false;
2049 }
2050 let frame_index = ((pts.saturating_sub(origin).max(0) as f64) * fps as f64 / 90_000.0) as usize;
2051 frame_index.saturating_add(4) < request_until.saturating_sub(MPEG2_STREAM_DECODE_LEAD_FRAMES)
2052}
2053
2054fn drain_mpeg2_stream_state(
2055 path: &Path,
2056 state: &mut Mpeg2StreamState,
2057 target_frame_idx: Option<usize>,
2058 target_timer_ms: u64,
2059) -> Result<()> {
2060 state.decoded_any_this_poll = false;
2061
2062 let decode_until =
2063 target_frame_idx.map(|idx| idx.saturating_add(MPEG2_STREAM_DECODE_LEAD_FRAMES));
2064
2065 for _ in 0..MPEG2_STREAM_MAX_DRAIN_EVENTS {
2066 if let Some(limit) = decode_until
2067 && state.decoded_frames > limit
2068 && !state.frames.is_empty()
2069 {
2070 break;
2071 }
2072 match state.rx.try_recv() {
2073 Ok(Ok(Mpeg2StreamEvent::Info { width, height, fps })) => {
2074 state.width = width.or(state.width);
2075 state.height = height.or(state.height);
2076 state.fps = fps.or(state.fps);
2077 }
2078 Ok(Ok(Mpeg2StreamEvent::Video {
2079 frame_idx,
2080 pts_90k,
2081 frame,
2082 })) => {
2083 if state.first_video_pts_90k.is_none() {
2084 state.first_video_pts_90k = pts_90k;
2085 }
2086 let mut decoded = Mpeg2DecodedFrame {
2087 frame_idx,
2088 pts_90k,
2089 frame,
2090 };
2091 if let Some(timeline_ms) = mpeg_frame_timeline_ms(&decoded, state) {
2092 if let Some(fps) = state.fps.filter(|value| *value > 0.0) {
2093 decoded.frame_idx = ((timeline_ms as f64) * fps as f64 / 1000.0)
2094 .round()
2095 .max(0.0) as usize;
2096 }
2097 state.last_video_timeline_ms = Some(
2098 state
2099 .last_video_timeline_ms
2100 .map(|last| last.max(timeline_ms))
2101 .unwrap_or(timeline_ms),
2102 );
2103 }
2104 state.decoded_frames = state
2105 .decoded_frames
2106 .max(decoded.frame_idx.saturating_add(1));
2107 state.frames.push_back(decoded);
2108 state.decoded_any_this_poll = true;
2109 }
2110 Ok(Ok(Mpeg2StreamEvent::Done)) => {
2111 state.done = true;
2112 break;
2113 }
2114 Ok(Err(err)) => {
2115 return Err(anyhow!(
2116 "mpeg2 stream decode failed for {}: {}",
2117 path.display(),
2118 err
2119 ));
2120 }
2121 Err(TryRecvError::Empty) => break,
2122 Err(TryRecvError::Disconnected) => {
2123 state.done = true;
2124 break;
2125 }
2126 }
2127 }
2128
2129 while state.frames.len() > 2 {
2130 let discard = state
2131 .frames
2132 .get(1)
2133 .and_then(|frame| mpeg_frame_timeline_ms(frame, state))
2134 .map(|next_ms| next_ms.saturating_add(100) < target_timer_ms)
2135 .unwrap_or_else(|| {
2136 target_frame_idx
2137 .zip(state.frames.get(1).map(|frame| frame.frame_idx))
2138 .map(|(target, next)| next.saturating_add(2) < target)
2139 .unwrap_or(false)
2140 });
2141 if !discard {
2142 break;
2143 }
2144 state.frames.pop_front();
2145 }
2146 while state.frames.len() > MPEG2_STREAM_FRAME_KEEP {
2147 state.frames.pop_front();
2148 }
2149
2150 Ok(())
2151}
2152
2153fn mpeg_timeline_origin_90k(state: &Mpeg2StreamState) -> Option<i64> {
2154 let video = state.first_video_pts_90k.or_else(|| {
2155 state
2156 .audio
2157 .as_ref()
2158 .and_then(|track| track.mpeg_stream.as_ref())
2159 .and_then(|stream| stream.first_video_pts_90k)
2160 });
2161 let audio = state
2162 .audio
2163 .as_ref()
2164 .and_then(|track| track.mpeg_stream.as_ref())
2165 .map(|stream| stream.first_audio_pts_90k);
2166 match (video, audio) {
2167 (Some(video), Some(audio)) => Some(earlier_mpeg_pts_90k(video, audio)),
2168 (Some(video), None) => Some(video),
2169 (None, Some(audio)) => Some(audio),
2170 (None, None) => None,
2171 }
2172}
2173
2174fn mpeg_frame_timeline_ms(frame: &Mpeg2DecodedFrame, state: &Mpeg2StreamState) -> Option<u64> {
2175 if let (Some(pts), Some(origin)) = (frame.pts_90k, mpeg_timeline_origin_90k(state)) {
2176 return mpeg_pts_delta_90k(pts, origin)
2177 .map(|ticks| ((ticks as i128) * 1000 / 90_000).max(0) as u64);
2178 }
2179 state
2180 .fps
2181 .filter(|fps| *fps > 0.0)
2182 .map(|fps| ((frame.frame_idx as f64) * 1000.0 / fps as f64).round() as u64)
2183}
2184
2185fn reindex_mpeg_stream_frames(state: &mut Mpeg2StreamState) {
2186 let origin = mpeg_timeline_origin_90k(state);
2187 let fps = state.fps.filter(|value| *value > 0.0);
2188 let mut last_timeline = None;
2189 for frame in &mut state.frames {
2190 let timeline_ms = match (frame.pts_90k, origin) {
2191 (Some(pts), Some(origin)) => mpeg_pts_delta_90k(pts, origin)
2192 .map(|ticks| ((ticks as i128) * 1000 / 90_000).max(0) as u64),
2193 _ => fps.map(|fps| ((frame.frame_idx as f64) * 1000.0 / fps as f64).round() as u64),
2194 };
2195 if let Some(timeline_ms) = timeline_ms {
2196 if let Some(fps) = fps {
2197 frame.frame_idx = ((timeline_ms as f64) * fps as f64 / 1000.0)
2198 .round()
2199 .max(0.0) as usize;
2200 }
2201 last_timeline = Some(
2202 last_timeline
2203 .map(|last: u64| last.max(timeline_ms))
2204 .unwrap_or(timeline_ms),
2205 );
2206 }
2207 }
2208 if let Some(last) = last_timeline {
2209 state.last_video_timeline_ms = Some(
2210 state
2211 .last_video_timeline_ms
2212 .map(|current| current.max(last))
2213 .unwrap_or(last),
2214 );
2215 }
2216 state.decoded_frames = state
2217 .frames
2218 .iter()
2219 .map(|frame| frame.frame_idx.saturating_add(1))
2220 .max()
2221 .unwrap_or(state.decoded_frames);
2222}
2223
2224fn select_mpeg_stream_frame<'a>(
2225 frames: &'a VecDeque<Mpeg2DecodedFrame>,
2226 state: &Mpeg2StreamState,
2227 target_ms: u64,
2228 fallback_idx: Option<usize>,
2229) -> Option<&'a Mpeg2DecodedFrame> {
2230 let mut before = None;
2231 let mut after = None;
2232 for frame in frames {
2233 if let Some(frame_ms) = mpeg_frame_timeline_ms(frame, state) {
2234 if frame_ms <= target_ms {
2235 before = Some(frame);
2236 } else {
2237 after = Some(frame);
2238 break;
2239 }
2240 }
2241 }
2242 if let Some(frame) = before.or(after) {
2243 return Some(frame);
2244 }
2245
2246 let chosen_idx =
2247 fallback_idx.unwrap_or_else(|| frames.back().map(|f| f.frame_idx).unwrap_or(0));
2248 frames
2249 .iter()
2250 .rev()
2251 .find(|frame| frame.frame_idx <= chosen_idx)
2252 .or_else(|| frames.front())
2253}
2254
2255fn is_wmv_extension(ext: &str) -> bool {
2256 ext == "wmv"
2257}
2258
2259fn probe_wmv_movie_info(path: &Path) -> Result<MovieInfo> {
2260 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
2261 let decoder = {
2262 let bytes = read_movie_bytes(path)?;
2263 wmv_decoder::AsfWmv2Decoder::open(Cursor::new(bytes))
2264 .with_context(|| format!("open ASF/WMV: {}", path.display()))?
2265 };
2266 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
2267 let decoder = {
2268 let file =
2269 fs::File::open(path).with_context(|| format!("open ASF/WMV: {}", path.display()))?;
2270 wmv_decoder::AsfWmv2Decoder::open(BufReader::new(file))
2271 .with_context(|| format!("parse ASF/WMV: {}", path.display()))?
2272 };
2273 let duration_ms = decoder.duration_ms();
2274 let video = decoder.video_stream_info();
2275 Ok(MovieInfo {
2276 path: path.to_path_buf(),
2277 width: (video.width > 0).then_some(video.width),
2278 height: (video.height > 0).then_some(video.height),
2279 fps: None,
2280 decoded_frames: None,
2281 audio_duration_ms: duration_ms,
2284 })
2285}
2286
2287fn spawn_wmv_stream_state(
2288 path: PathBuf,
2289 audio: Option<MovieAudio>,
2290 target_ms: u64,
2291) -> Result<WmvStreamState> {
2292 let info = probe_wmv_movie_info(&path)?;
2293 let (tx, rx) = mpsc::sync_channel(WMV_STREAM_CHANNEL_CAPACITY);
2294 let request_ms = Arc::new(AtomicUsize::new(
2295 target_ms
2296 .saturating_add(WMV_STREAM_DECODE_LEAD_MS as u64)
2297 .min(usize::MAX as u64) as usize,
2298 ));
2299 let worker_request_ms = request_ms.clone();
2300 let worker_path = path.clone();
2301 thread::spawn(move || {
2302 lower_worker_thread_priority();
2303 let result = stream_wmv_video_worker(worker_path.as_path(), tx.clone(), worker_request_ms);
2304 if let Err(err) = result {
2305 let _ = tx.send(Err(format!("{:#}", err)));
2306 }
2307 });
2308
2309 Ok(WmvStreamState {
2310 rx,
2311 frames: VecDeque::new(),
2312 width: info.width,
2313 height: info.height,
2314 fps: None,
2315 decoded_frames: 0,
2316 timeline_origin_ms: None,
2317 last_video_source_pts_ms: None,
2318 last_frame_delta_ms: None,
2319 total_ms_hint: info.duration_ms(),
2320 last_effective_timer_ms: target_ms,
2321 done: false,
2322 audio,
2323 decoded_any_this_poll: false,
2324 request_ms,
2325 #[cfg(target_os = "vita")]
2326 idle_frames: 0,
2327 })
2328}
2329
2330fn stream_wmv_video_worker(
2331 path: &Path,
2332 tx: mpsc::SyncSender<Result<WmvStreamEvent, String>>,
2333 request_ms: Arc<AtomicUsize>,
2334) -> Result<()> {
2335 let file =
2336 fs::File::open(path).with_context(|| format!("open WMV movie file: {}", path.display()))?;
2337 let mut decoder = wmv_decoder::AsfWmv2Decoder::open(BufReader::new(file))
2338 .with_context(|| format!("open WMV video decoder: {}", path.display()))?;
2339 let info = decoder.video_stream_info().clone();
2340 if tx
2341 .send(Ok(WmvStreamEvent::Info {
2342 width: info.width,
2343 height: info.height,
2344 }))
2345 .is_err()
2346 {
2347 return Ok(());
2348 }
2349
2350 let trace = env_is_set!("SG_MOVIE_TRACE");
2351 if trace {
2352 eprintln!(
2353 "[SG_MOVIE_TRACE][WMV] decoder.open path={} size={}x{}",
2354 path.display(),
2355 info.width,
2356 info.height
2357 );
2358 }
2359
2360 let mut frame_idx = 0usize;
2361 loop {
2362 if request_ms.load(Ordering::Acquire) == usize::MAX {
2372 return Ok(());
2373 }
2374 let decoded = decoder
2375 .next_frame()
2376 .with_context(|| format!("decode WMV video: {}", path.display()))?;
2377 let Some(decoded) = decoded else {
2378 break;
2379 };
2380 let source_pts_ms = decoded.pts_ms as u64;
2381
2382 if trace && (frame_idx < 5 || frame_idx.is_multiple_of(60)) {
2383 eprintln!(
2384 "[SG_MOVIE_TRACE][WMV] decoded idx={} pts={}",
2385 frame_idx, source_pts_ms
2386 );
2387 }
2388
2389 let frame = Arc::new(wmv_yuv_frame_to_rgba(&decoded.frame));
2390 if tx
2391 .send(Ok(WmvStreamEvent::Video {
2392 frame_idx,
2393 source_pts_ms,
2394 frame,
2395 }))
2396 .is_err()
2397 {
2398 return Ok(());
2399 }
2400 frame_idx = frame_idx.saturating_add(1);
2401 }
2402
2403 let _ = tx.send(Ok(WmvStreamEvent::Done));
2404 Ok(())
2405}
2406
2407fn drain_wmv_stream_state(
2408 path: &Path,
2409 state: &mut WmvStreamState,
2410 target_timer_ms: u64,
2411) -> Result<()> {
2412 state.decoded_any_this_poll = false;
2413
2414 if let Some(origin_ms) = state.timeline_origin_ms {
2426 discard_wmv_stream_frames(state, origin_ms.saturating_add(target_timer_ms));
2427 }
2428
2429 let mut video_budget = WMV_STREAM_FRAME_KEEP.saturating_sub(state.frames.len());
2430 for _ in 0..WMV_STREAM_MAX_DRAIN_EVENTS {
2431 if video_budget == 0 {
2437 break;
2438 }
2439 match state.rx.try_recv() {
2440 Ok(Ok(WmvStreamEvent::Info { width, height })) => {
2441 state.width = (width > 0).then_some(width).or(state.width);
2442 state.height = (height > 0).then_some(height).or(state.height);
2443 }
2444 Ok(Ok(WmvStreamEvent::Video {
2445 frame_idx,
2446 source_pts_ms,
2447 frame,
2448 })) => {
2449 state.last_video_source_pts_ms = Some(
2450 state
2451 .last_video_source_pts_ms
2452 .map(|last| last.max(source_pts_ms))
2453 .unwrap_or(source_pts_ms),
2454 );
2455 state.decoded_frames = state.decoded_frames.max(frame_idx.saturating_add(1));
2456 let decoded = WmvDecodedFrame {
2457 frame_idx,
2458 source_pts_ms,
2459 frame,
2460 };
2461 let insert_at = state
2465 .frames
2466 .iter()
2467 .position(|queued| queued.source_pts_ms > decoded.source_pts_ms)
2468 .unwrap_or(state.frames.len());
2469 state.frames.insert(insert_at, decoded);
2470 video_budget = video_budget.saturating_sub(1);
2471
2472 if insert_at > 0 {
2475 let prev = state.frames[insert_at - 1].source_pts_ms;
2476 let cur = state.frames[insert_at].source_pts_ms;
2477 let delta = cur.saturating_sub(prev);
2478 if (2..=1_000).contains(&delta) {
2479 state.last_frame_delta_ms = Some(delta);
2480 state.fps = Some(1000.0 / delta as f32);
2481 }
2482 }
2483 if insert_at + 1 < state.frames.len() {
2484 let cur = state.frames[insert_at].source_pts_ms;
2485 let next = state.frames[insert_at + 1].source_pts_ms;
2486 let delta = next.saturating_sub(cur);
2487 if (2..=1_000).contains(&delta) {
2488 state.last_frame_delta_ms = Some(delta);
2489 state.fps = Some(1000.0 / delta as f32);
2490 }
2491 }
2492 state.decoded_any_this_poll = true;
2493 }
2494 Ok(Ok(WmvStreamEvent::Done)) => {
2495 state.done = true;
2496 break;
2497 }
2498 Ok(Err(err)) => {
2499 return Err(anyhow!(
2500 "wmv stream decode failed for {}: {}",
2501 path.display(),
2502 err
2503 ));
2504 }
2505 Err(TryRecvError::Empty) => break,
2506 Err(TryRecvError::Disconnected) => {
2507 state.done = true;
2508 break;
2509 }
2510 }
2511 }
2512
2513 if let Some(origin_ms) = state.timeline_origin_ms {
2518 discard_wmv_stream_frames(state, origin_ms.saturating_add(target_timer_ms));
2519 }
2520 Ok(())
2521}
2522
2523fn discard_wmv_stream_frames(state: &mut WmvStreamState, target_source_pts_ms: u64) {
2524 while state.frames.len() > 2 {
2525 let discard = state
2526 .frames
2527 .get(1)
2528 .map(|frame| frame.source_pts_ms.saturating_add(100) < target_source_pts_ms)
2529 .unwrap_or(false);
2530 if !discard {
2531 break;
2532 }
2533 state.frames.pop_front();
2534 }
2535 }
2539
2540fn select_wmv_stream_frame(
2541 frames: &VecDeque<WmvDecodedFrame>,
2542 target_source_pts_ms: u64,
2543) -> Option<&WmvDecodedFrame> {
2544 let mut before = None;
2549 for frame in frames {
2550 if frame.source_pts_ms <= target_source_pts_ms {
2551 before = Some(frame);
2552 } else {
2553 break;
2554 }
2555 }
2556 before
2557}
2558
2559fn wmv_yuv_frame_to_rgba(frame: &wmv_decoder::YuvFrame) -> RgbaImage {
2560 let (width, height) = movie_output_dimensions(frame.width, frame.height);
2564 let rgba = wmv_decoder::yuv420p_to_rgba_scaled(frame, width, height);
2565 RgbaImage {
2566 width,
2567 height,
2568 center_x: 0,
2569 center_y: 0,
2570 rgba,
2571 }
2572}
2573
2574fn spawn_omv_stream_state(path: PathBuf) -> Result<OmvStreamState> {
2575 let (tx, rx) = mpsc::sync_channel(OMV_STREAM_CHANNEL_CAPACITY);
2576 let request_frame = Arc::new(AtomicUsize::new(0));
2577 let worker_request_frame = request_frame.clone();
2578 thread::spawn(move || {
2579 lower_worker_thread_priority();
2580 let result = stream_omv_video_worker(path.as_path(), tx.clone(), worker_request_frame);
2581 if let Err(err) = result {
2582 let _ = tx.send(Err(format!("{:#}", err)));
2583 }
2584 });
2585 Ok(OmvStreamState {
2586 rx,
2587 frames: VecDeque::new(),
2588 loop_head_frames: VecDeque::new(),
2589 loop_head_bytes: 0,
2590 width: None,
2591 height: None,
2592 fps: None,
2593 frame_time_ms: None,
2594 total_frames_hint: None,
2595 total_ms_hint: None,
2596 frame_times: None,
2597 decoded_frames: 0,
2598 done: false,
2599 request_frame,
2600 last_served_frame_idx: 0,
2601 last_effective_timer_ms: 0,
2602 held_frame: None,
2603 #[cfg(target_os = "vita")]
2604 idle_frames: 0,
2605 })
2606}
2607
2608fn stream_omv_video_worker(
2609 path: &Path,
2610 tx: mpsc::SyncSender<Result<OmvStreamEvent, String>>,
2611 request_frame: Arc<AtomicUsize>,
2612) -> Result<()> {
2613 let omv = siglus_assets::omv::OmvFile::open(path)
2614 .with_context(|| format!("open OMV index: {}", path.display()))?;
2615 let reader = omv
2616 .open_embedded_ogg_reader(path)
2617 .with_context(|| format!("open embedded Ogg stream: {}", path.display()))?;
2618 let mut video_tf = siglus_omv_decoder::TheoraVideoStream::open(reader)
2619 .with_context(|| format!("open streaming Theora decoder: {}", path.display()))?;
2620 let vinfo = video_tf.info();
2621
2622 let display_w = omv.header.display_width as i32;
2623 let display_h = omv.header.display_height as i32;
2624 let width = display_w.max(1) as u32;
2625 let height = display_h.max(1) as u32;
2626 let fps = if omv.header.frame_time_us != 0 {
2627 Some(1_000_000.0 / (omv.header.frame_time_us as f32))
2628 } else if vinfo.fps > 0.0 {
2629 Some(vinfo.fps as f32)
2630 } else {
2631 None
2632 };
2633 let frame_time_ms = omv_frame_duration_ms(Some(&omv.header), fps);
2634 let total_frames_hint = (!omv.packets.is_empty())
2635 .then_some(omv.packets.len())
2636 .or_else(|| {
2637 (omv.header.packet_count_hint > 0).then_some(omv.header.packet_count_hint as usize)
2638 });
2639 let frame_times = Arc::new(
2640 omv.packets
2641 .iter()
2642 .map(|packet| {
2643 (
2644 u64::try_from(packet.frame_time_start.max(0)).unwrap_or(0),
2645 u64::try_from(packet.frame_time_end.max(0)).unwrap_or(0),
2646 )
2647 })
2648 .collect::<Vec<_>>(),
2649 );
2650 let total_ms_hint = frame_times
2651 .last()
2652 .map(|(_, end)| *end)
2653 .filter(|end| *end > 0);
2654 let theora_type = omv.header.theora_type;
2655 let mut pool = OmvFramePool::default();
2656
2657 if tx
2658 .send(Ok(OmvStreamEvent::Info {
2659 width,
2660 height,
2661 fps,
2662 frame_time_ms,
2663 total_frames_hint,
2664 total_ms_hint,
2665 frame_times,
2666 }))
2667 .is_err()
2668 {
2669 return Ok(());
2670 }
2671
2672 let total_frames = total_frames_hint.unwrap_or(0);
2673 let mut next_frame_idx = 0usize;
2674 let mut last_requested_frame = 0usize;
2675 let mut eof_reported = false;
2676
2677 loop {
2678 let requested = request_frame.load(Ordering::Acquire);
2679 if requested == usize::MAX {
2680 return Ok(());
2681 }
2682 let target_frame = if total_frames > 0 {
2683 requested.min(total_frames.saturating_sub(1))
2684 } else {
2685 requested
2686 };
2687 let indexed_seek = omv_should_index_seek(&omv, last_requested_frame, target_frame);
2688
2689 if indexed_seek {
2690 let seek_result = omv.seek_point_for_frame(target_frame).and_then(|point| {
2691 video_tf.seek_to_indexed_frame(
2692 point.file_offset,
2693 point.key_page_file_offset,
2694 point.first_packet_no,
2695 point.key_frame_packet_no,
2696 point.target_packet_no,
2697 )
2698 });
2699 let packed = match seek_result {
2700 Ok(frame) => frame,
2701 Err(index_err) => {
2702 eprintln!(
2703 "[SG_MOV] OMV indexed seek fallback path={} frame={} err={:#}",
2704 path.display(),
2705 target_frame,
2706 index_err
2707 );
2708 let reader = omv.open_embedded_ogg_reader(path).with_context(|| {
2709 format!("reopen embedded Ogg stream: {}", path.display())
2710 })?;
2711 let mut restarted = siglus_omv_decoder::TheoraVideoStream::open(reader)
2712 .with_context(|| {
2713 format!("restart streaming Theora decoder: {}", path.display())
2714 })?;
2715 let mut frame = None;
2716 for _ in 0..=target_frame {
2717 frame = restarted.read_video_frame()?;
2718 if frame.is_none() {
2719 break;
2720 }
2721 }
2722 video_tf = restarted;
2723 frame
2724 }
2725 };
2726
2727 if tx
2728 .send(Ok(OmvStreamEvent::Reset {
2729 frame_idx: target_frame,
2730 }))
2731 .is_err()
2732 {
2733 return Ok(());
2734 }
2735 if let Some(buf) = packed
2736 && !send_omv_video_frame(
2737 &tx,
2738 &mut pool,
2739 target_frame,
2740 &buf,
2741 vinfo,
2742 display_h,
2743 theora_type,
2744 width,
2745 height,
2746 )?
2747 {
2748 return Ok(());
2749 }
2750 next_frame_idx = target_frame.saturating_add(1);
2754 eof_reported = false;
2755 }
2756
2757 last_requested_frame = target_frame;
2758 let decode_until = if total_frames > 0 {
2759 target_frame
2760 .saturating_add(OMV_STREAM_DECODE_LEAD_FRAMES)
2761 .min(total_frames.saturating_sub(1))
2762 } else {
2763 target_frame.saturating_add(OMV_STREAM_DECODE_LEAD_FRAMES)
2764 };
2765 let mut did_work = false;
2766 while next_frame_idx <= decode_until {
2767 if request_frame.load(Ordering::Acquire) == usize::MAX {
2768 return Ok(());
2769 }
2770 let Some(frame) = video_tf.read_video_frame_with(|ycbcr| {
2771 omv_rgba_frame(
2772 &DecoderOmvPlanes(ycbcr),
2773 vinfo,
2774 display_h,
2775 theora_type,
2776 width,
2777 height,
2778 &mut pool,
2779 )
2780 })?
2781 else {
2782 if !eof_reported {
2783 if tx.send(Ok(OmvStreamEvent::Done)).is_err() {
2784 return Ok(());
2785 }
2786 eof_reported = true;
2787 }
2788 break;
2789 };
2790 if tx
2791 .send(Ok(OmvStreamEvent::Video {
2792 frame_idx: next_frame_idx,
2793 frame,
2794 }))
2795 .is_err()
2796 {
2797 return Ok(());
2798 }
2799 next_frame_idx = next_frame_idx.saturating_add(1);
2800 did_work = true;
2801 }
2802
2803 if !did_work {
2804 thread::sleep(Duration::from_millis(1));
2805 }
2806 }
2807}
2808
2809fn omv_should_index_seek(
2810 omv: &siglus_assets::omv::OmvFile,
2811 current_frame: usize,
2812 target_frame: usize,
2813) -> bool {
2814 if target_frame < current_frame {
2815 return true;
2816 }
2817 let Some(current_packet) = omv.packets.get(current_frame) else {
2818 return false;
2819 };
2820 let Some(target_packet) = omv.packets.get(target_frame) else {
2821 return false;
2822 };
2823 if current_packet.key_frame_packet_no == target_packet.key_frame_packet_no {
2824 return false;
2825 }
2826 let Ok(key_page_no) = usize::try_from(target_packet.key_frame_page_no) else {
2827 return false;
2828 };
2829 let Some(key_page) = omv.pages.get(key_page_no) else {
2830 return false;
2831 };
2832 current_packet.own_page_no < key_page.seek_page_no
2833}
2834
2835fn send_omv_video_frame(
2836 tx: &mpsc::SyncSender<Result<OmvStreamEvent, String>>,
2837 pool: &mut OmvFramePool,
2838 frame_idx: usize,
2839 buf: &[u8],
2840 vinfo: siglus_omv_decoder::VideoInfo,
2841 display_h: i32,
2842 theora_type: u32,
2843 width: u32,
2844 height: u32,
2845) -> Result<bool> {
2846 let (uv_w, _uv_h, y_plane_len, u_plane_len, _v_plane_len) =
2847 omv_plane_layout(vinfo.frame_width, vinfo.frame_height, vinfo.fmt);
2848 let planes = PackedOmvPlanes {
2849 data: buf,
2850 offsets: [0, y_plane_len, y_plane_len.saturating_add(u_plane_len)],
2851 widths: [vinfo.frame_width.max(1) as usize, uv_w, uv_w],
2852 };
2853 let frame = omv_rgba_frame(&planes, vinfo, display_h, theora_type, width, height, pool);
2854 Ok(tx
2855 .send(Ok(OmvStreamEvent::Video { frame_idx, frame }))
2856 .is_ok())
2857}
2858
2859fn omv_rgba_frame(
2862 planes: &impl OmvPlanes,
2863 vinfo: siglus_omv_decoder::VideoInfo,
2864 display_h: i32,
2865 theora_type: u32,
2866 width: u32,
2867 height: u32,
2868 pool: &mut OmvFramePool,
2869) -> Arc<RgbaImage> {
2870 let (output_width, output_height) = movie_output_dimensions(width, height);
2871 let mut frame = pool.take().unwrap_or_else(|| {
2872 Arc::new(RgbaImage {
2873 width: 0,
2874 height: 0,
2875 center_x: 0,
2876 center_y: 0,
2877 rgba: Vec::new(),
2878 })
2879 });
2880 let image = Arc::get_mut(&mut frame).expect("a pooled frame is held only by the pool");
2881 convert_omv_planes_into(
2882 planes,
2883 vinfo.frame_width,
2884 vinfo.frame_height,
2885 vinfo.fmt,
2886 width as i32,
2887 display_h,
2888 theora_type,
2889 output_width as usize,
2890 output_height as usize,
2891 &mut image.rgba,
2892 );
2893 image.width = output_width;
2894 image.height = output_height;
2895 pool.track(&frame);
2896 frame
2897}
2898
2899#[derive(Default)]
2903struct OmvFramePool {
2904 sent: VecDeque<Arc<RgbaImage>>,
2905}
2906
2907impl OmvFramePool {
2908 const MAX_TRACKED: usize = 64;
2910
2911 fn take(&mut self) -> Option<Arc<RgbaImage>> {
2914 let unique = |frame: &Arc<RgbaImage>| {
2915 Arc::strong_count(frame) == 1 && Arc::weak_count(frame) == 0
2916 };
2917 let index = self.sent.iter().position(unique)?;
2918 let frame = self.sent.remove(index);
2919 self.sent.retain(|frame| !unique(frame));
2920 frame
2921 }
2922
2923 fn track(&mut self, frame: &Arc<RgbaImage>) {
2924 self.sent.push_back(frame.clone());
2925 if self.sent.len() > Self::MAX_TRACKED {
2926 self.sent.pop_front();
2927 }
2928 }
2929}
2930
2931fn select_stream_frame(
2932 frames: &VecDeque<(usize, Arc<RgbaImage>)>,
2933 chosen_idx: usize,
2934) -> Option<(usize, Arc<RgbaImage>)> {
2935 let (front_idx, _) = frames.front()?;
2936 let (back_idx, back_frame) = frames.back()?;
2937 if chosen_idx < *front_idx {
2938 let (_, front_frame) = frames.front()?;
2939 return Some((*front_idx, front_frame.clone()));
2940 }
2941 if chosen_idx >= *back_idx {
2942 return Some((*back_idx, back_frame.clone()));
2943 }
2944
2945 let direct = chosen_idx.saturating_sub(*front_idx);
2946 if let Some((idx, frame)) = frames.get(direct)
2947 && *idx == chosen_idx
2948 {
2949 return Some((*idx, frame.clone()));
2950 }
2951
2952 let mut i = direct.min(frames.len().saturating_sub(1));
2953 loop {
2954 if let Some((idx, frame)) = frames.get(i)
2955 && *idx <= chosen_idx
2956 {
2957 return Some((*idx, frame.clone()));
2958 }
2959 if i == 0 {
2960 break;
2961 }
2962 i -= 1;
2963 }
2964
2965 Some((*back_idx, back_frame.clone()))
2966}
2967
2968fn select_omv_loop_frame(
2969 state: &OmvStreamState,
2970 chosen_idx: usize,
2971) -> Option<(usize, Arc<RgbaImage>)> {
2972 let live_queue_is_after_target = state
2973 .frames
2974 .front()
2975 .map(|(idx, _)| *idx > chosen_idx)
2976 .unwrap_or(true);
2977 let target_is_in_cached_head = state
2978 .loop_head_frames
2979 .back()
2980 .map(|(idx, _)| chosen_idx <= *idx)
2981 .unwrap_or(false);
2982 if live_queue_is_after_target || target_is_in_cached_head {
2983 if let Some(frame) = select_stream_frame(&state.loop_head_frames, chosen_idx) {
2987 return Some(frame);
2988 }
2989 }
2990 select_stream_frame(&state.frames, chosen_idx)
2991 .or_else(|| select_stream_frame(&state.loop_head_frames, chosen_idx))
2992}
2993
2994fn cache_omv_loop_head_frame(state: &mut OmvStreamState, frame_idx: usize, frame: &Arc<RgbaImage>) {
2995 if frame_idx >= OMV_LOOP_HEAD_CACHE_MAX_FRAMES
2996 || state
2997 .loop_head_frames
2998 .iter()
2999 .any(|(cached_idx, _)| *cached_idx == frame_idx)
3000 {
3001 return;
3002 }
3003 let frame_bytes = frame.rgba.len();
3004 if !state.loop_head_frames.is_empty()
3005 && state.loop_head_bytes.saturating_add(frame_bytes) > OMV_LOOP_HEAD_CACHE_MAX_BYTES
3006 {
3007 return;
3008 }
3009 state.loop_head_bytes = state.loop_head_bytes.saturating_add(frame_bytes);
3010 state.loop_head_frames.push_back((frame_idx, frame.clone()));
3011}
3012
3013#[cfg(feature = "virtual-clock")]
3017fn settle_omv_stream(
3018 path: &Path,
3019 state: &mut OmvStreamState,
3020 target_frame_idx: Option<usize>,
3021 ready: impl Fn(&OmvStreamState) -> bool,
3022) -> Result<()> {
3023 let start = std::time::Instant::now();
3024 let deadline = start + std::time::Duration::from_secs(5);
3025 while !ready(state) && !state.done && std::time::Instant::now() < deadline {
3026 std::thread::sleep(std::time::Duration::from_millis(1));
3027 drain_omv_stream_state(path, state, target_frame_idx, false, true)?;
3028 }
3029 OMV_SETTLE_NS.fetch_add(
3030 start.elapsed().as_nanos() as u64,
3031 std::sync::atomic::Ordering::Relaxed,
3032 );
3033 Ok(())
3034}
3035
3036#[cfg(feature = "virtual-clock")]
3039pub static OMV_SETTLE_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
3040
3041fn drain_omv_stream_state(
3042 path: &Path,
3043 state: &mut OmvStreamState,
3044 target_frame_idx: Option<usize>,
3045 retain_from_start: bool,
3046 cache_loop_head: bool,
3047) -> Result<()> {
3048 for _ in 0..OMV_STREAM_MAX_DRAIN_EVENTS {
3049 match state.rx.try_recv() {
3050 Ok(Ok(OmvStreamEvent::Info {
3051 width,
3052 height,
3053 fps,
3054 frame_time_ms,
3055 total_frames_hint,
3056 total_ms_hint,
3057 frame_times,
3058 })) => {
3059 state.width = Some(width);
3060 state.height = Some(height);
3061 state.fps = fps.or(state.fps);
3062 state.frame_time_ms = frame_time_ms.or(state.frame_time_ms);
3063 state.total_frames_hint = total_frames_hint.or(state.total_frames_hint);
3064 state.total_ms_hint = total_ms_hint.or(state.total_ms_hint);
3065 state.frame_times = Some(frame_times);
3066 }
3067 Ok(Ok(OmvStreamEvent::Reset { frame_idx })) => {
3068 state.frames.clear();
3069 state.decoded_frames = frame_idx;
3070 state.done = false;
3071 }
3072 Ok(Ok(OmvStreamEvent::Video { frame_idx, frame })) => {
3073 state.decoded_frames = state.decoded_frames.max(frame_idx.saturating_add(1));
3074 if cache_loop_head {
3075 cache_omv_loop_head_frame(state, frame_idx, &frame);
3076 }
3077 state.frames.push_back((frame_idx, frame));
3078 }
3079 Ok(Ok(OmvStreamEvent::Done)) => {
3080 state.done = true;
3084 }
3085 Ok(Err(err)) => {
3086 return Err(anyhow!(
3087 "omv stream decode failed for {}: {}",
3088 path.display(),
3089 err
3090 ));
3091 }
3092 Err(TryRecvError::Empty) => break,
3093 Err(TryRecvError::Disconnected) => {
3094 state.done = true;
3095 break;
3096 }
3097 }
3098 }
3099
3100 if !retain_from_start {
3101 if let Some(target) = target_frame_idx {
3102 let keep_from = target.saturating_sub(2);
3103 while state
3104 .frames
3105 .front()
3106 .map(|(idx, _)| *idx < keep_from)
3107 .unwrap_or(false)
3108 {
3109 state.frames.pop_front();
3110 }
3111 }
3112 while state.frames.len() > OMV_STREAM_FRAME_KEEP {
3113 state.frames.pop_front();
3114 }
3115 }
3116 Ok(())
3117}
3118
3119fn omv_frame_for_time(frame_times: &[(u64, u64)], now_ms: u64) -> Option<usize> {
3120 if frame_times.is_empty() {
3121 return None;
3122 }
3123 let mut low = 0usize;
3124 let mut high = frame_times.len();
3125 while low < high {
3126 let mid = low + (high - low) / 2;
3127 if now_ms <= frame_times[mid].1 {
3128 high = mid;
3129 } else {
3130 low = mid.saturating_add(1);
3131 }
3132 }
3133 Some(low.min(frame_times.len().saturating_sub(1)))
3134}
3135
3136fn omv_frame_duration_ms(
3137 header: Option<&siglus_assets::omv::OmvHeader>,
3138 fps: Option<f32>,
3139) -> Option<f64> {
3140 if let Some(h) = header
3141 && h.frame_time_us != 0
3142 {
3143 return Some((h.frame_time_us as f64) / 1000.0);
3144 }
3145 let f = fps?;
3146 if f > 0.0 {
3147 Some(1000.0 / (f as f64))
3148 } else {
3149 None
3150 }
3151}
3152
3153fn omv_plane_layout(
3154 frame_width: i32,
3155 frame_height: i32,
3156 fmt: i32,
3157) -> (usize, usize, usize, usize, usize) {
3158 let w = frame_width.max(1) as usize;
3164 let h = frame_height.max(1) as usize;
3165 let y_len = w.saturating_mul(h);
3166 let (uv_w, uv_h) = yuv_plane_size(frame_width, frame_height, fmt);
3167 let uv_len = uv_w.saturating_mul(uv_h);
3168 (uv_w, uv_h, y_len, uv_len, uv_len)
3169}
3170
3171#[derive(Debug, Clone)]
3172pub struct MovieAsset {
3173 pub info: MovieInfo,
3174 pub frames: Vec<Arc<RgbaImage>>,
3175 pub audio: Option<MovieAudio>,
3176}
3177
3178#[derive(Debug, Clone)]
3179pub struct MpegStreamAudio {
3180 pub path: Arc<PathBuf>,
3181 pub num_frames: usize,
3182 pub first_audio_pts_ms: i64,
3183 pub first_video_pts_ms: Option<i64>,
3184 pub first_audio_pts_90k: i64,
3185 pub first_video_pts_90k: Option<i64>,
3186 pub audio_end_pts_90k: i64,
3187 pub file_len: u64,
3188 pub transport_stream: bool,
3189}
3190
3191#[derive(Debug, Clone)]
3192pub struct WmvStreamAudio {
3193 pub path: Arc<PathBuf>,
3194 pub num_frames: usize,
3195 pub initial_silence_frames: usize,
3196 pub source_skip_frames: usize,
3197}
3198
3199#[derive(Debug, Clone)]
3200pub struct MovieAudio {
3201 pub samples: Arc<Vec<i16>>,
3203 pub mpeg_stream: Option<MpegStreamAudio>,
3207 pub wmv_stream: Option<WmvStreamAudio>,
3210 pub channels: u16,
3211 pub sample_rate: u32,
3212 pub start_ms: u64,
3213 pub duration_ms: Option<u64>,
3214}
3215
3216impl MovieAudio {
3217 fn end_ms(&self) -> u64 {
3218 self.start_ms.saturating_add(self.duration_ms.unwrap_or(0))
3219 }
3220}
3221
3222fn make_static_movie_playback(
3223 audio: &mut AudioHub,
3224 track: &MovieAudio,
3225 local_offset_ms: u64,
3226 loop_flag: bool,
3227) -> Result<(MoviePlaybackHandle, u64)> {
3228 let wav = encode_wav_i16_interleaved(track.samples.as_ref(), track.channels, track.sample_rate);
3229 let mut data = StaticSoundData::from_cursor(Cursor::new(wav))
3230 .context("kira: decode movie WAV bytes")?
3231 .start_position(local_offset_ms as f64 / 1000.0);
3232 if loop_flag {
3233 data = data.loop_region(..);
3234 }
3235 Ok((
3236 MoviePlaybackHandle::Static(audio.play_static(TrackKind::Mov, data)?),
3237 track.start_ms,
3238 ))
3239}
3240
3241#[derive(Debug)]
3242enum MoviePlaybackHandle {
3243 Static(StaticSoundHandle),
3244 #[cfg(not(target_arch = "wasm32"))]
3245 Streaming(StreamingSoundHandle<anyhow::Error>),
3246}
3247
3248#[derive(Debug)]
3249struct MoviePlayback {
3250 handle: MoviePlaybackHandle,
3251 timeline_base_ms: u64,
3254}
3255
3256impl MoviePlayback {
3257 fn state(&self) -> PlaybackState {
3258 match &self.handle {
3259 MoviePlaybackHandle::Static(handle) => handle.state(),
3260 #[cfg(not(target_arch = "wasm32"))]
3261 MoviePlaybackHandle::Streaming(handle) => handle.state(),
3262 }
3263 }
3264
3265 fn movie_position_ms(&self) -> u64 {
3266 let seconds = match &self.handle {
3267 MoviePlaybackHandle::Static(handle) => handle.position(),
3268 #[cfg(not(target_arch = "wasm32"))]
3269 MoviePlaybackHandle::Streaming(handle) => handle.position(),
3270 };
3271 self.timeline_base_ms
3272 .saturating_add((seconds.max(0.0) * 1000.0).round() as u64)
3273 }
3274
3275 fn pause(&mut self) {
3276 match &mut self.handle {
3277 MoviePlaybackHandle::Static(handle) => {
3278 handle.pause(kira::tween::Tween::default());
3279 }
3280 #[cfg(not(target_arch = "wasm32"))]
3281 MoviePlaybackHandle::Streaming(handle) => {
3282 handle.pause(kira::tween::Tween::default());
3283 }
3284 }
3285 }
3286
3287 fn resume(&mut self) {
3288 match &mut self.handle {
3289 MoviePlaybackHandle::Static(handle) => {
3290 handle.resume(kira::tween::Tween::default());
3291 }
3292 #[cfg(not(target_arch = "wasm32"))]
3293 MoviePlaybackHandle::Streaming(handle) => {
3294 handle.resume(kira::tween::Tween::default());
3295 }
3296 }
3297 }
3298
3299 fn stop(&mut self) {
3300 match &mut self.handle {
3301 MoviePlaybackHandle::Static(handle) => {
3302 handle.stop(kira::tween::Tween::default());
3303 }
3304 #[cfg(not(target_arch = "wasm32"))]
3305 MoviePlaybackHandle::Streaming(handle) => {
3306 handle.stop(kira::tween::Tween::default());
3307 }
3308 }
3309 }
3310
3311 fn take_stream_error(&mut self) -> Option<anyhow::Error> {
3312 match &mut self.handle {
3313 MoviePlaybackHandle::Static(_) => None,
3314 #[cfg(not(target_arch = "wasm32"))]
3315 MoviePlaybackHandle::Streaming(handle) => handle.pop_error(),
3316 }
3317 }
3318}
3319
3320#[cfg(not(target_arch = "wasm32"))]
3321const WMV_AUDIO_DECODE_FRAMES: usize = 4096;
3322
3323#[cfg(not(target_arch = "wasm32"))]
3324struct WmvMovieAudioDecoder {
3325 info: WmvStreamAudio,
3326 sample_rate: u32,
3327 decoder: wmv_decoder::AsfWmaDecoder<BufReader<fs::File>>,
3328 pending: VecDeque<Frame>,
3329 initial_silence_remaining: usize,
3330 source_skip_remaining: usize,
3331 produced_frames: usize,
3332 eof: bool,
3333}
3334
3335#[cfg(not(target_arch = "wasm32"))]
3336impl WmvMovieAudioDecoder {
3337 fn new(info: WmvStreamAudio, sample_rate: u32) -> Result<Self> {
3338 if sample_rate == 0 || info.num_frames == 0 {
3339 bail!("invalid WMV movie audio stream description");
3340 }
3341 let decoder = Self::open_decoder(info.path.as_ref())?;
3342 let initial_silence_remaining = info.initial_silence_frames;
3343 let source_skip_remaining = info.source_skip_frames;
3344 Ok(Self {
3345 info,
3346 sample_rate,
3347 decoder,
3348 pending: VecDeque::new(),
3349 initial_silence_remaining,
3350 source_skip_remaining,
3351 produced_frames: 0,
3352 eof: false,
3353 })
3354 }
3355
3356 fn open_decoder(path: &Path) -> Result<wmv_decoder::AsfWmaDecoder<BufReader<fs::File>>> {
3357 let file = fs::File::open(path)
3358 .with_context(|| format!("open WMV movie audio: {}", path.display()))?;
3359 wmv_decoder::AsfWmaDecoder::open(BufReader::new(file))
3360 .with_context(|| format!("open WMA decoder: {}", path.display()))
3361 }
3362
3363 fn reset(&mut self) -> Result<()> {
3364 self.decoder = Self::open_decoder(self.info.path.as_ref())?;
3365 self.pending.clear();
3366 self.initial_silence_remaining = self.info.initial_silence_frames;
3367 self.source_skip_remaining = self.info.source_skip_frames;
3368 self.produced_frames = 0;
3369 self.eof = false;
3370 Ok(())
3371 }
3372
3373 fn decode_more(&mut self) -> Result<()> {
3374 if self.eof {
3375 return Ok(());
3376 }
3377 match self.decoder.next_frame()? {
3378 Some(decoded) => {
3379 let frames = convert_movie_audio_chunk_to_frames(
3380 &decoded.frame.samples,
3381 decoded.frame.channels,
3382 decoded.frame.sample_rate,
3383 self.sample_rate,
3384 );
3385 let skip = self.source_skip_remaining.min(frames.len());
3386 self.source_skip_remaining -= skip;
3387 self.pending.extend(frames.into_iter().skip(skip));
3388 }
3389 None => self.eof = true,
3390 }
3391 Ok(())
3392 }
3393
3394 fn prime_to_timeline_frame(&mut self, target: usize) -> Result<usize> {
3399 let target = target.min(self.info.num_frames.saturating_sub(1));
3400 let mut advanced = 0usize;
3401
3402 let silence = target.min(self.initial_silence_remaining);
3403 self.initial_silence_remaining -= silence;
3404 advanced = advanced.saturating_add(silence);
3405
3406 while advanced < target && !self.eof {
3407 if self.pending.is_empty() {
3408 self.decode_more()?;
3409 }
3410 while advanced < target {
3411 let Some(_frame) = self.pending.pop_front() else {
3412 break;
3413 };
3414 advanced = advanced.saturating_add(1);
3415 }
3416 }
3417 self.produced_frames = advanced;
3418 Ok(advanced)
3419 }
3420}
3421
3422#[cfg(not(target_arch = "wasm32"))]
3423impl KiraStreamingDecoder for WmvMovieAudioDecoder {
3424 type Error = anyhow::Error;
3425
3426 fn sample_rate(&self) -> u32 {
3427 self.sample_rate
3428 }
3429
3430 fn num_frames(&self) -> usize {
3431 self.info.num_frames
3432 }
3433
3434 fn decode(&mut self) -> Result<Vec<Frame>, Self::Error> {
3435 let remaining = self.info.num_frames.saturating_sub(self.produced_frames);
3436 if remaining == 0 {
3437 return Ok(Vec::new());
3438 }
3439 let target = remaining.min(WMV_AUDIO_DECODE_FRAMES);
3440 let mut out = Vec::with_capacity(target);
3441 while out.len() < target {
3442 if self.initial_silence_remaining > 0 {
3443 let count = self
3444 .initial_silence_remaining
3445 .min(target.saturating_sub(out.len()));
3446 out.resize(out.len().saturating_add(count), Frame::ZERO);
3447 self.initial_silence_remaining -= count;
3448 continue;
3449 }
3450 while out.len() < target {
3451 let Some(frame) = self.pending.pop_front() else {
3452 break;
3453 };
3454 out.push(frame);
3455 }
3456 if out.len() >= target {
3457 break;
3458 }
3459 if self.eof {
3460 out.resize(target, Frame::ZERO);
3464 break;
3465 }
3466 self.decode_more()?;
3467 }
3468 self.produced_frames = self.produced_frames.saturating_add(out.len());
3469 Ok(out)
3470 }
3471
3472 fn seek(&mut self, index: usize) -> Result<usize, Self::Error> {
3473 self.reset()?;
3474 self.prime_to_timeline_frame(index)
3475 }
3476}
3477
3478#[cfg(not(target_arch = "wasm32"))]
3479const MPEG_AUDIO_DECODE_FRAMES: usize = 4096;
3480
3481#[cfg(not(target_arch = "wasm32"))]
3482struct MpegMovieAudioDecoder {
3483 info: MpegStreamAudio,
3484 sample_rate: u32,
3485 file: fs::File,
3486 pipeline: na_mpeg2_decoder::MpegAudioPipeline,
3487 read_buf: Vec<u8>,
3488 pending: VecDeque<Frame>,
3489 initial_silence_frames: usize,
3490 produced_frames: usize,
3491 eof: bool,
3492}
3493
3494#[cfg(not(target_arch = "wasm32"))]
3495impl MpegMovieAudioDecoder {
3496 fn new(info: MpegStreamAudio, sample_rate: u32) -> Result<Self> {
3497 if sample_rate == 0 || info.num_frames == 0 {
3498 bail!("invalid MPEG movie audio stream description");
3499 }
3500 let file = fs::File::open(info.path.as_ref())
3501 .with_context(|| format!("open MPEG movie audio: {}", info.path.display()))?;
3502 let initial_silence_frames = mpeg_initial_silence_frames(&info, sample_rate);
3503 Ok(Self {
3504 info,
3505 sample_rate,
3506 file,
3507 pipeline: na_mpeg2_decoder::MpegAudioPipeline::new(),
3508 read_buf: vec![0u8; MPEG2_STREAM_CHUNK_BYTES],
3509 pending: VecDeque::new(),
3510 initial_silence_frames,
3511 produced_frames: 0,
3512 eof: false,
3513 })
3514 }
3515
3516 fn reset(&mut self) -> Result<()> {
3517 self.reset_at_offset(0, 0)
3518 }
3519
3520 fn reset_at_offset(&mut self, offset: u64, produced_frames: usize) -> Result<()> {
3521 self.file = fs::File::open(self.info.path.as_ref())
3522 .with_context(|| format!("reopen MPEG movie audio: {}", self.info.path.display()))?;
3523 if offset > 0 {
3524 self.file
3525 .seek(SeekFrom::Start(offset))
3526 .with_context(|| format!("seek MPEG movie audio: {}", self.info.path.display()))?;
3527 }
3528 self.pipeline = na_mpeg2_decoder::MpegAudioPipeline::new();
3529 self.pending.clear();
3530 self.initial_silence_frames = if offset == 0 {
3531 mpeg_initial_silence_frames(&self.info, self.sample_rate)
3532 } else {
3533 0
3534 };
3535 self.produced_frames = produced_frames;
3536 self.eof = false;
3537 Ok(())
3538 }
3539
3540 fn seek_start_offset(&self, target_audio_frames: usize, backtrack: u64) -> Result<u64> {
3541 let initial_silence = mpeg_initial_silence_frames(&self.info, self.sample_rate);
3542 let audio_frames = self.info.num_frames.saturating_sub(initial_silence).max(1);
3543 let proportional = ((self.info.file_len as u128)
3544 .saturating_mul(target_audio_frames.min(audio_frames) as u128)
3545 / audio_frames as u128)
3546 .min(self.info.file_len as u128) as u64;
3547 let start = proportional.saturating_sub(backtrack.min(proportional));
3548 align_mpeg_container_seek(self.info.path.as_ref(), start, self.info.transport_stream)
3549 }
3550
3551 fn prime_seek_from_offset(
3552 &mut self,
3553 offset: u64,
3554 target_index: usize,
3555 ) -> Result<Option<usize>> {
3556 self.reset_at_offset(offset, 0)?;
3557 let mut bytes_read = 0u64;
3558 while bytes_read < MPEG_AUDIO_SEEK_MAX_PRIME_BYTES {
3559 let n = self.file.read(&mut self.read_buf).with_context(|| {
3560 format!("prime MPEG movie audio seek: {}", self.info.path.display())
3561 })?;
3562 if n == 0 {
3563 break;
3564 }
3565 bytes_read = bytes_read.saturating_add(n as u64);
3566 let input = self.read_buf[..n].to_vec();
3567 let mut chunks = Vec::new();
3568 self.pipeline
3569 .push_with(&input, None, |chunk| chunks.push(chunk))
3570 .context("prime MPEG movie audio decoder")?;
3571 let mut actual_index = None;
3572 for chunk in chunks {
3573 if actual_index.is_none() {
3574 let Some(chunk_index) =
3575 mpeg_audio_chunk_timeline_frame(&self.info, chunk.pts_ms, self.sample_rate)
3576 else {
3577 continue;
3578 };
3579 if chunk_index > target_index {
3580 return Ok(None);
3581 }
3582 actual_index = Some(chunk_index);
3583 self.initial_silence_frames = 0;
3584 self.produced_frames = chunk_index;
3585 }
3586 let converted = convert_movie_audio_chunk_to_frames(
3587 &chunk.samples,
3588 chunk.channels,
3589 chunk.sample_rate,
3590 self.sample_rate,
3591 );
3592 self.pending.extend(converted);
3593 }
3594 if let Some(actual) = actual_index.filter(|_| !self.pending.is_empty()) {
3595 return Ok(Some(actual));
3596 }
3597 }
3598 Ok(None)
3599 }
3600
3601 fn append_chunk(&mut self, chunk: na_mpeg2_decoder::MpegAudioF32) {
3602 let converted = convert_movie_audio_chunk_to_frames(
3603 &chunk.samples,
3604 chunk.channels,
3605 chunk.sample_rate,
3606 self.sample_rate,
3607 );
3608 self.pending.extend(converted);
3609 }
3610
3611 fn decode_more(&mut self) -> Result<()> {
3612 if self.eof {
3613 return Ok(());
3614 }
3615 let n = self
3616 .file
3617 .read(&mut self.read_buf)
3618 .with_context(|| format!("read MPEG movie audio: {}", self.info.path.display()))?;
3619 if n == 0 {
3620 let mut chunks = Vec::new();
3621 self.pipeline
3622 .flush_with(|chunk| chunks.push(chunk))
3623 .context("flush MPEG movie audio")?;
3624 for chunk in chunks {
3625 self.append_chunk(chunk);
3626 }
3627 self.eof = true;
3628 return Ok(());
3629 }
3630
3631 let input = self.read_buf[..n].to_vec();
3634 let mut chunks = Vec::new();
3635 self.pipeline
3636 .push_with(&input, None, |chunk| chunks.push(chunk))
3637 .context("decode MPEG movie audio chunk")?;
3638 for chunk in chunks {
3639 self.append_chunk(chunk);
3640 }
3641 Ok(())
3642 }
3643}
3644
3645#[cfg(not(target_arch = "wasm32"))]
3646impl KiraStreamingDecoder for MpegMovieAudioDecoder {
3647 type Error = anyhow::Error;
3648
3649 fn sample_rate(&self) -> u32 {
3650 self.sample_rate
3651 }
3652
3653 fn num_frames(&self) -> usize {
3654 self.info.num_frames
3655 }
3656
3657 fn decode(&mut self) -> Result<Vec<Frame>, Self::Error> {
3658 let remaining = self.info.num_frames.saturating_sub(self.produced_frames);
3659 if remaining == 0 {
3660 return Ok(Vec::new());
3661 }
3662 let target = remaining.min(MPEG_AUDIO_DECODE_FRAMES);
3663 let mut out = Vec::with_capacity(target);
3664
3665 while out.len() < target {
3666 if self.initial_silence_frames > 0 {
3667 let count = self
3668 .initial_silence_frames
3669 .min(target.saturating_sub(out.len()));
3670 out.resize(out.len().saturating_add(count), Frame::ZERO);
3671 self.initial_silence_frames -= count;
3672 continue;
3673 }
3674
3675 while out.len() < target {
3676 let Some(frame) = self.pending.pop_front() else {
3677 break;
3678 };
3679 out.push(frame);
3680 }
3681 if out.len() >= target {
3682 break;
3683 }
3684
3685 if self.eof {
3686 out.resize(target, Frame::ZERO);
3691 break;
3692 }
3693 self.decode_more()?;
3694 }
3695
3696 self.produced_frames = self.produced_frames.saturating_add(out.len());
3697 Ok(out)
3698 }
3699
3700 fn seek(&mut self, index: usize) -> Result<usize, Self::Error> {
3701 let target = index.min(self.info.num_frames.saturating_sub(1));
3702 let initial_silence = mpeg_initial_silence_frames(&self.info, self.sample_rate);
3703 if target <= initial_silence || self.info.file_len == 0 {
3704 self.reset()?;
3705 return Ok(0);
3706 }
3707
3708 let target_audio_frames = target.saturating_sub(initial_silence);
3709 let mut backtrack = MPEG_AUDIO_SEEK_INITIAL_BACKTRACK_BYTES;
3710 loop {
3711 let offset = self.seek_start_offset(target_audio_frames, backtrack)?;
3712 if let Some(actual) = self.prime_seek_from_offset(offset, target)? {
3713 return Ok(actual);
3714 }
3715 if offset == 0 || backtrack >= MPEG_AUDIO_SEEK_MAX_PRIME_BYTES {
3716 break;
3717 }
3718 backtrack = backtrack
3719 .saturating_mul(4)
3720 .min(MPEG_AUDIO_SEEK_MAX_PRIME_BYTES);
3721 }
3722
3723 self.reset()?;
3724 Ok(0)
3725 }
3726}
3727
3728#[cfg(not(target_arch = "wasm32"))]
3729fn align_mpeg_container_seek(path: &Path, offset: u64, transport_stream: bool) -> Result<u64> {
3730 if offset == 0 {
3731 return Ok(0);
3732 }
3733 if transport_stream {
3734 return Ok(offset - offset % 188);
3735 }
3736
3737 let file_len = fs::metadata(path)
3738 .with_context(|| format!("stat MPEG seek source: {}", path.display()))?
3739 .len();
3740 let mut file = fs::File::open(path)
3741 .with_context(|| format!("open MPEG seek source: {}", path.display()))?;
3742 file.seek(SeekFrom::Start(offset))
3743 .with_context(|| format!("seek MPEG source: {}", path.display()))?;
3744 let probe_len = (1024 * 1024u64).min(file_len.saturating_sub(offset)) as usize;
3745 let mut probe = vec![0u8; probe_len];
3746 let n = file
3747 .read(&mut probe)
3748 .with_context(|| format!("read MPEG seek source: {}", path.display()))?;
3749 probe.truncate(n);
3750 if let Some(pos) = find_byte_pattern(&probe, b"\0\0\x01\xba")
3751 .or_else(|| find_byte_pattern(&probe, b"\0\0\x01\xb3"))
3752 {
3753 Ok(offset.saturating_add(pos as u64))
3754 } else {
3755 Ok(offset)
3756 }
3757}
3758
3759#[cfg(not(target_arch = "wasm32"))]
3760fn mpeg_audio_chunk_timeline_frame(
3761 info: &MpegStreamAudio,
3762 chunk_pts_ms: i64,
3763 sample_rate: u32,
3764) -> Option<usize> {
3765 if sample_rate == 0 || (chunk_pts_ms == 0 && info.first_audio_pts_ms != 0) {
3766 return None;
3767 }
3768 let origin = match info.first_video_pts_90k {
3769 Some(video) if mpeg_pts_delta_90k(video, info.first_audio_pts_90k).is_none() => video,
3770 Some(_) | None => info.first_audio_pts_90k,
3771 };
3772 let chunk_pts_90k = chunk_pts_ms.saturating_mul(90);
3773 let ticks = mpeg_pts_delta_90k(chunk_pts_90k, origin)?;
3774 Some(pts90k_ticks_to_audio_frames(ticks, sample_rate))
3775}
3776
3777#[cfg(not(target_arch = "wasm32"))]
3778fn mpeg_initial_silence_frames(info: &MpegStreamAudio, sample_rate: u32) -> usize {
3779 let origin = info
3780 .first_video_pts_90k
3781 .map(|video| earlier_mpeg_pts_90k(video, info.first_audio_pts_90k))
3782 .unwrap_or(info.first_audio_pts_90k);
3783 pts90k_ticks_to_audio_frames(
3784 mpeg_pts_delta_90k(info.first_audio_pts_90k, origin).unwrap_or(0),
3785 sample_rate,
3786 )
3787}
3788
3789#[cfg(not(target_arch = "wasm32"))]
3790fn convert_movie_audio_chunk_to_frames(
3791 samples: &[f32],
3792 src_channels: u16,
3793 src_sample_rate: u32,
3794 dst_sample_rate: u32,
3795) -> Vec<Frame> {
3796 let src_channels = src_channels as usize;
3797 if src_channels == 0 || src_sample_rate == 0 || dst_sample_rate == 0 {
3798 return Vec::new();
3799 }
3800 let src_frames = samples.len() / src_channels;
3801 if src_frames == 0 {
3802 return Vec::new();
3803 }
3804 let dst_frames = ((src_frames as u128) * (dst_sample_rate as u128) / (src_sample_rate as u128))
3805 .max(1) as usize;
3806 let mut out = Vec::with_capacity(dst_frames);
3807
3808 for dst_index in 0..dst_frames {
3809 let src_num = (dst_index as u128) * (src_sample_rate as u128);
3810 let src_index = (src_num / dst_sample_rate as u128) as usize;
3811 let src_index = src_index.min(src_frames - 1);
3812 let next_index = src_index.saturating_add(1).min(src_frames - 1);
3813 let fraction = (src_num % dst_sample_rate as u128) as f32 / dst_sample_rate as f32;
3814 let sample = |frame: usize, channel: usize| -> f32 {
3815 samples
3816 .get(
3817 frame
3818 .saturating_mul(src_channels)
3819 .saturating_add(channel.min(src_channels - 1)),
3820 )
3821 .copied()
3822 .unwrap_or(0.0)
3823 };
3824 let interpolate = |channel: usize| {
3825 let a = sample(src_index, channel);
3826 let b = sample(next_index, channel);
3827 a + (b - a) * fraction
3828 };
3829 let left = interpolate(0);
3830 let right = if src_channels == 1 {
3831 left
3832 } else {
3833 interpolate(1)
3834 };
3835 out.push(Frame::new(left, right));
3836 }
3837 out
3838}
3839
3840#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
3841fn read_movie_bytes(path: &Path) -> Result<Vec<u8>> {
3842 crate::resource::read_file_bytes(path)
3843 .with_context(|| format!("read movie file: {}", path.display()))
3844}
3845
3846#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
3847fn read_movie_bytes(path: &Path) -> Result<Vec<u8>> {
3848 crate::resource::read_file_bytes(path)
3849 .with_context(|| format!("read movie file: {}", path.display()))
3850}
3851
3852#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
3853fn read_omv_header_for_path(path: &Path) -> Result<siglus_assets::omv::OmvHeader> {
3854 let bytes = read_movie_bytes(path)?;
3855 read_omv_header_from_bytes(&bytes)
3856}
3857
3858#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
3859fn read_omv_header_for_path(path: &Path) -> Result<siglus_assets::omv::OmvHeader> {
3860 siglus_assets::omv::OmvFile::read_header(path)
3861}
3862
3863fn read_omv_header_from_bytes(buf: &[u8]) -> Result<siglus_assets::omv::OmvHeader> {
3864 if buf.len() < 0x58 {
3865 bail!("OMV header too small");
3866 }
3867 let header_size = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
3868 let version = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
3869 let theora_type = u32::from_le_bytes([buf[0x28], buf[0x29], buf[0x2a], buf[0x2b]]);
3870 let display_width = u32::from_le_bytes([buf[0x2c], buf[0x2d], buf[0x2e], buf[0x2f]]);
3871 let display_height = u32::from_le_bytes([buf[0x30], buf[0x31], buf[0x32], buf[0x33]]);
3872 let frame_time_us = u32::from_le_bytes([buf[0x3c], buf[0x3d], buf[0x3e], buf[0x3f]]);
3873 let max_data_size = u32::from_le_bytes([buf[0x40], buf[0x41], buf[0x42], buf[0x43]]);
3874 let page_count_hint = u32::from_le_bytes([buf[0x4c], buf[0x4d], buf[0x4e], buf[0x4f]]);
3875 let packet_count_hint = u32::from_le_bytes([buf[0x50], buf[0x51], buf[0x52], buf[0x53]]);
3876 if header_size < 0x58 {
3877 bail!("invalid OMV header size: {header_size:#x}");
3878 }
3879 if theora_type > siglus_assets::omv::OMV_THEORA_TYPE_YUV {
3880 bail!("invalid OMV theora type: {theora_type}");
3881 }
3882 if display_width == 0 || display_height == 0 {
3883 bail!(
3884 "invalid OMV display size: {}x{}",
3885 display_width,
3886 display_height
3887 );
3888 }
3889 Ok(siglus_assets::omv::OmvHeader {
3890 header_size,
3891 version,
3892 theora_type,
3893 display_width,
3894 display_height,
3895 frame_time_us,
3896 max_data_size,
3897 page_count_hint,
3898 packet_count_hint,
3899 })
3900}
3901
3902fn extract_ogg_from_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
3903 let needle = b"OggS";
3904 let pos = bytes
3905 .windows(needle.len())
3906 .position(|w| w == needle)
3907 .ok_or_else(|| anyhow!("OggS not found in OMV payload"))?;
3908 Ok(bytes[pos..].to_vec())
3909}
3910
3911#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
3912fn decode_mpeg2_asset_from_bytes(path: &Path, bytes: Vec<u8>) -> Result<MovieAsset> {
3913 let mut width = None;
3914 let mut height = None;
3915 let mut fps = None;
3916 if let Some(h) = siglus_assets::mpeg2::find_sequence_header(
3917 &bytes[..bytes.len().min(MPEG2_HEADER_PROBE_BYTES)],
3918 ) {
3919 width = Some(h.width as u32);
3920 height = Some(h.height as u32);
3921 fps = siglus_assets::mpeg2::fps_from_frame_rate_code(h.frame_rate_code);
3922 }
3923
3924 let mut frames: Vec<Arc<RgbaImage>> = Vec::new();
3925 let mut audio_samples: Vec<i16> = Vec::new();
3926 let mut audio_channels: Option<u16> = None;
3927 let mut audio_sample_rate: Option<u32> = None;
3928 let mut dropped_audio_format_changes = 0u32;
3929 let mut pipeline = na_mpeg2_decoder::MpegAvPipeline::new();
3930 pipeline
3931 .push_with(&bytes, None, |ev| match ev {
3932 na_mpeg2_decoder::MpegAvEvent::Video(f) => {
3933 let w = f.width;
3934 let h = f.height;
3935 frames.push(Arc::new(RgbaImage {
3936 width: w,
3937 height: h,
3938 center_x: 0,
3939 center_y: 0,
3940 rgba: f.rgba,
3941 }));
3942 }
3943 na_mpeg2_decoder::MpegAvEvent::Audio(a) => {
3944 append_mpeg2_audio_chunk_for_asset(
3945 path,
3946 &mut audio_channels,
3947 &mut audio_sample_rate,
3948 &mut audio_samples,
3949 &mut dropped_audio_format_changes,
3950 a,
3951 );
3952 }
3953 })
3954 .context("mpeg2 wasm full decode")?;
3955 pipeline.flush_with(|ev| match ev {
3956 na_mpeg2_decoder::MpegAvEvent::Video(f) => {
3957 let w = f.width;
3958 let h = f.height;
3959 frames.push(Arc::new(RgbaImage {
3960 width: w,
3961 height: h,
3962 center_x: 0,
3963 center_y: 0,
3964 rgba: f.rgba,
3965 }));
3966 }
3967 na_mpeg2_decoder::MpegAvEvent::Audio(a) => {
3968 append_mpeg2_audio_chunk_for_asset(
3969 path,
3970 &mut audio_channels,
3971 &mut audio_sample_rate,
3972 &mut audio_samples,
3973 &mut dropped_audio_format_changes,
3974 a,
3975 );
3976 }
3977 })?;
3978
3979 if frames.is_empty() {
3980 bail!("mpeg2 decoder produced no frames: {}", path.display());
3981 }
3982 let audio =
3983 build_movie_audio_from_parts(path, audio_samples, audio_channels, audio_sample_rate)?;
3984 let audio_duration_ms = audio.as_ref().and_then(|a| a.duration_ms);
3985 let first = frames.first().expect("frames not empty");
3986 let info = MovieInfo {
3987 path: path.to_path_buf(),
3988 width: width.or(Some(first.width)),
3989 height: height.or(Some(first.height)),
3990 fps,
3991 decoded_frames: Some(frames.len()),
3992 audio_duration_ms,
3993 };
3994 Ok(MovieAsset {
3995 info,
3996 frames,
3997 audio,
3998 })
3999}
4000
4001#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
4002fn decode_wmv_asset_from_bytes(path: &Path, bytes: Vec<u8>) -> Result<MovieAsset> {
4003 let mut decoder = wmv_decoder::AsfWmv2Decoder::open(Cursor::new(bytes.clone()))
4004 .with_context(|| format!("open ASF/WMV decoder: {}", path.display()))?;
4005 let stream_info = decoder.video_stream_info().clone();
4006 let mut frames = Vec::<Arc<RgbaImage>>::new();
4007 let mut pts = Vec::<u64>::new();
4008 while let Some(decoded) = decoder
4009 .next_frame()
4010 .with_context(|| format!("decode ASF/WMV frame: {}", path.display()))?
4011 {
4012 pts.push(decoded.pts_ms as u64);
4013 frames.push(Arc::new(wmv_yuv_frame_to_rgba(&decoded.frame)));
4014 }
4015 if frames.is_empty() {
4016 bail!("wmv decoder produced no frames: {}", path.display());
4017 }
4018
4019 let timeline_origin_ms = pts.first().copied().unwrap_or(0);
4020 for pts_ms in &mut pts {
4021 *pts_ms = pts_ms.saturating_sub(timeline_origin_ms);
4022 }
4023 let cancel = AtomicBool::new(false);
4024 let audio = decode_wmv_audio_full_from_reader(Cursor::new(bytes), &cancel, timeline_origin_ms)?;
4025 let fps = wmv_fps_from_pts(&pts);
4026 let video_duration_ms = pts
4027 .last()
4028 .copied()
4029 .map(|last| last.saturating_add(wmv_frame_duration_ms(&pts).unwrap_or(0)));
4030 let audio_duration_ms = audio.as_ref().map(MovieAudio::end_ms);
4031 let total_ms = match (audio_duration_ms, video_duration_ms) {
4032 (Some(a), Some(v)) => Some(a.max(v)),
4033 (Some(a), None) => Some(a),
4034 (None, Some(v)) => Some(v),
4035 (None, None) => None,
4036 };
4037 let info = MovieInfo {
4038 path: path.to_path_buf(),
4039 width: (stream_info.width > 0).then_some(stream_info.width),
4040 height: (stream_info.height > 0).then_some(stream_info.height),
4041 fps,
4042 decoded_frames: Some(frames.len()),
4043 audio_duration_ms: total_ms,
4044 };
4045 Ok(MovieAsset {
4046 info,
4047 frames,
4048 audio,
4049 })
4050}
4051
4052#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
4053fn decode_omv_asset_from_bytes(path: &Path, bytes: Vec<u8>) -> Result<MovieAsset> {
4054 let header = read_omv_header_from_bytes(&bytes).ok();
4055 let ogg_data = extract_ogg_from_bytes(&bytes)
4056 .with_context(|| format!("read embedded ogg: {}", path.display()))?;
4057 let mut tf = siglus_omv_decoder::TheoraFile::open_from_memory(ogg_data)
4058 .with_context(|| format!("open theora: {}", path.display()))?;
4059 let vinfo = tf.info();
4060 let display_w = header
4061 .as_ref()
4062 .map(|h| h.display_width as i32)
4063 .unwrap_or(vinfo.width);
4064 let display_h = header
4065 .as_ref()
4066 .map(|h| h.display_height as i32)
4067 .unwrap_or(vinfo.height);
4068 let width = display_w.max(1) as u32;
4069 let height = display_h.max(1) as u32;
4070 let fps = header
4071 .as_ref()
4072 .and_then(|h| {
4073 if h.frame_time_us != 0 {
4074 Some(1_000_000.0 / (h.frame_time_us as f32))
4075 } else if vinfo.fps > 0.0 {
4076 Some(vinfo.fps as f32)
4077 } else {
4078 None
4079 }
4080 })
4081 .or_else(|| (vinfo.fps > 0.0).then_some(vinfo.fps as f32));
4082 let theora_type = header
4083 .as_ref()
4084 .map(|h| h.theora_type)
4085 .unwrap_or(siglus_assets::omv::OMV_THEORA_TYPE_YUV);
4086 let (_uv_w, _uv_h, y_len, u_len, v_len) =
4087 omv_plane_layout(vinfo.frame_width, vinfo.frame_height, vinfo.fmt);
4088 let mut packed = vec![0u8; y_len.saturating_add(u_len).saturating_add(v_len)];
4089 let mut frames = Vec::<Arc<RgbaImage>>::new();
4090 while tf.read_video_frame(&mut packed)? {
4091 let rgba = convert_omv_frame(
4092 &packed,
4093 vinfo.frame_width,
4094 vinfo.frame_height,
4095 vinfo.fmt,
4096 display_w,
4097 display_h,
4098 theora_type,
4099 );
4100 frames.push(Arc::new(RgbaImage {
4101 width,
4102 height,
4103 center_x: 0,
4104 center_y: 0,
4105 rgba,
4106 }));
4107 }
4108 if frames.is_empty() {
4109 bail!("omv decoder produced no frames: {}", path.display());
4110 }
4111 tf.reset();
4112 let audio = decode_omv_audio(&mut tf)?;
4113 let frame_time_ms = omv_frame_duration_ms(header.as_ref(), fps);
4114 let video_duration_ms =
4115 frame_time_ms.map(|ms| ((frames.len() as f64) * ms).round().max(1.0) as u64);
4116 let audio_duration_ms = audio
4117 .as_ref()
4118 .and_then(|a| a.duration_ms)
4119 .or(video_duration_ms);
4120 let info = MovieInfo {
4121 path: path.to_path_buf(),
4122 width: Some(width),
4123 height: Some(height),
4124 fps,
4125 decoded_frames: Some(frames.len()),
4126 audio_duration_ms,
4127 };
4128 Ok(MovieAsset {
4129 info,
4130 frames,
4131 audio,
4132 })
4133}
4134
4135#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
4136fn append_mpeg2_audio_chunk_for_asset(
4137 _path: &Path,
4138 audio_channels: &mut Option<u16>,
4139 audio_sample_rate: &mut Option<u32>,
4140 audio_samples: &mut Vec<i16>,
4141 dropped_audio_format_changes: &mut u32,
4142 a: na_mpeg2_decoder::MpegAudioF32,
4143) {
4144 match (*audio_channels, *audio_sample_rate) {
4145 (None, None) => {
4146 *audio_channels = Some(a.channels);
4147 *audio_sample_rate = Some(a.sample_rate);
4148 }
4149 (Some(ch), Some(sr)) if ch == a.channels && sr == a.sample_rate => {}
4150 (Some(_), Some(_)) => {
4151 *dropped_audio_format_changes = (*dropped_audio_format_changes).saturating_add(1);
4152 return;
4153 }
4154 _ => return,
4155 }
4156 audio_samples.extend(a.samples.into_iter().map(f32_to_i16_sample));
4157}
4158
4159#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
4160fn build_movie_audio_from_parts(
4161 path: &Path,
4162 audio_samples: Vec<i16>,
4163 audio_channels: Option<u16>,
4164 audio_sample_rate: Option<u32>,
4165) -> Result<Option<MovieAudio>> {
4166 match (audio_channels, audio_sample_rate, audio_samples.is_empty()) {
4167 (Some(channels), Some(sample_rate), false) => {
4168 if channels == 0 || sample_rate == 0 {
4169 bail!(
4170 "movie audio stream has invalid format in {}: channels={} sample_rate={}",
4171 path.display(),
4172 channels,
4173 sample_rate
4174 );
4175 }
4176 let frames_len = (audio_samples.len() as u64) / (channels as u64);
4177 let duration_ms =
4178 Some(((frames_len as f64) * 1000.0 / sample_rate as f64).round() as u64);
4179 Ok(Some(MovieAudio {
4180 samples: Arc::new(audio_samples),
4181 mpeg_stream: None,
4182 wmv_stream: None,
4183 channels,
4184 sample_rate,
4185 start_ms: 0,
4186 duration_ms,
4187 }))
4188 }
4189 (None, None, true) => Ok(None),
4190 (Some(_), Some(_), true) => Ok(None),
4191 _ => bail!(
4192 "movie audio decoder produced incomplete format metadata for {}",
4193 path.display()
4194 ),
4195 }
4196}
4197
4198fn decode_asset_for_path(path: &Path) -> Result<MovieAsset> {
4199 let ext = path
4200 .extension()
4201 .and_then(|s| s.to_str())
4202 .unwrap_or("")
4203 .to_ascii_lowercase();
4204 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
4205 {
4206 let bytes = read_movie_bytes(path)?;
4207 if ext == "omv" {
4208 return decode_omv_asset_from_bytes(path, bytes);
4209 }
4210 if is_wmv_extension(&ext) {
4211 return decode_wmv_asset_from_bytes(path, bytes);
4212 }
4213 return decode_mpeg2_asset_from_bytes(path, bytes);
4214 }
4215 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
4216 {
4217 if ext == "omv" {
4218 decode_omv_asset(path)
4219 } else if is_wmv_extension(&ext) {
4220 decode_wmv_asset(path)
4221 } else {
4222 decode_mpeg2_asset(path)
4223 }
4224 }
4225}
4226
4227fn wmv_frame_duration_ms(pts: &[u64]) -> Option<u64> {
4228 let mut deltas = pts
4229 .windows(2)
4230 .filter_map(|pair| {
4231 let delta = pair[1].saturating_sub(pair[0]);
4232 (delta > 0 && delta <= 1_000).then_some(delta)
4233 })
4234 .collect::<Vec<_>>();
4235 if deltas.is_empty() {
4236 return None;
4237 }
4238 deltas.sort_unstable();
4239 Some(deltas[deltas.len() / 2])
4240}
4241
4242fn wmv_fps_from_pts(pts: &[u64]) -> Option<f32> {
4243 wmv_frame_duration_ms(pts)
4244 .filter(|delta| *delta > 0)
4245 .map(|delta| 1000.0 / delta as f32)
4246}
4247
4248fn wmv_rebase_duration_ms(source_duration_ms: u64, timeline_origin_ms: u64) -> Option<u64> {
4249 let rebased = source_duration_ms.saturating_sub(timeline_origin_ms);
4250 (rebased > 0).then_some(rebased)
4251}
4252
4253fn wmv_audio_alignment_frames(
4254 first_audio_pts_ms: u64,
4255 timeline_origin_ms: u64,
4256 sample_rate: u32,
4257) -> (usize, usize) {
4258 if sample_rate == 0 {
4259 return (0, 0);
4260 }
4261 let silence_ms = first_audio_pts_ms.saturating_sub(timeline_origin_ms);
4262 let skip_ms = timeline_origin_ms.saturating_sub(first_audio_pts_ms);
4263 let initial_silence_frames = ((silence_ms as u128) * sample_rate as u128)
4264 .div_ceil(1000)
4265 .min(usize::MAX as u128) as usize;
4266 let source_skip_frames =
4267 ((skip_ms as u128) * sample_rate as u128 / 1000).min(usize::MAX as u128) as usize;
4268 (initial_silence_frames, source_skip_frames)
4269}
4270
4271fn probe_wmv_first_video_pts_ms(path: &Path) -> Result<Option<u64>> {
4272 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
4273 let mut decoder = {
4274 let bytes = read_movie_bytes(path)?;
4275 wmv_decoder::AsfWmv2Decoder::open(Cursor::new(bytes))
4276 .with_context(|| format!("open ASF/WMV timeline probe: {}", path.display()))?
4277 };
4278 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
4279 let mut decoder = {
4280 let file = fs::File::open(path)
4281 .with_context(|| format!("open WMV timeline probe: {}", path.display()))?;
4282 wmv_decoder::AsfWmv2Decoder::open(BufReader::new(file))
4283 .with_context(|| format!("open WMV timeline probe decoder: {}", path.display()))?
4284 };
4285
4286 Ok(decoder
4287 .next_frame()
4288 .with_context(|| format!("decode WMV timeline probe: {}", path.display()))?
4289 .map(|frame| frame.pts_ms as u64))
4290}
4291
4292fn decode_wmv_preview_frame(path: &Path) -> Result<Arc<RgbaImage>> {
4293 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
4294 let mut decoder = {
4295 let bytes = read_movie_bytes(path)?;
4296 wmv_decoder::AsfWmv2Decoder::open(Cursor::new(bytes))
4297 .with_context(|| format!("open WMV preview decoder: {}", path.display()))?
4298 };
4299 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
4300 let mut decoder = {
4301 let file = fs::File::open(path)
4302 .with_context(|| format!("open WMV preview: {}", path.display()))?;
4303 wmv_decoder::AsfWmv2Decoder::open(BufReader::new(file))
4304 .with_context(|| format!("open WMV preview decoder: {}", path.display()))?
4305 };
4306
4307 let decoded = decoder
4308 .next_frame()
4309 .with_context(|| format!("decode WMV preview: {}", path.display()))?
4310 .ok_or_else(|| anyhow!("wmv preview frame missing: {}", path.display()))?;
4311 Ok(Arc::new(wmv_yuv_frame_to_rgba(&decoded.frame)))
4312}
4313
4314fn decode_mpeg2_preview_frame(path: &Path) -> Result<Arc<RgbaImage>> {
4315 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
4316 {
4317 let bytes = read_movie_bytes(path)?;
4318 let asset = decode_mpeg2_asset_from_bytes(path, bytes)?;
4319 return asset
4320 .frames
4321 .into_iter()
4322 .next()
4323 .ok_or_else(|| anyhow!("mpeg2 preview frame missing: {}", path.display()));
4324 }
4325
4326 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
4327 {
4328 let mut file =
4329 fs::File::open(path).with_context(|| format!("open movie file: {}", path.display()))?;
4330 let mut pipeline = na_mpeg2_decoder::MpegVideoPipeline::new();
4331 let mut first = None;
4332 let mut buf = vec![0u8; MPEG2_STREAM_CHUNK_BYTES];
4333 loop {
4334 let n = file
4335 .read(&mut buf)
4336 .with_context(|| format!("read movie preview stream: {}", path.display()))?;
4337 if n == 0 {
4338 break;
4339 }
4340 pipeline
4341 .push_with(&buf[..n], None, |f| {
4342 if first.is_none() {
4343 let w = f.width as u32;
4344 let h = f.height as u32;
4345 let (w, h) = movie_output_dimensions(w, h);
4346 let mut rgba = vec![0u8; (w as usize) * (h as usize) * 4];
4347 na_mpeg2_decoder::frame_to_rgba_bt601_limited_scaled(
4348 &f, &mut rgba, w as usize, h as usize,
4349 );
4350 first = Some(Arc::new(RgbaImage {
4351 width: w,
4352 height: h,
4353 center_x: 0,
4354 center_y: 0,
4355 rgba,
4356 }));
4357 }
4358 })
4359 .context("mpeg2 preview decode")?;
4360 if first.is_some() {
4361 break;
4362 }
4363 }
4364 if first.is_none() {
4365 pipeline.flush_with(|f| {
4366 if first.is_none() {
4367 let w = f.width as u32;
4368 let h = f.height as u32;
4369 let (w, h) = movie_output_dimensions(w, h);
4370 let mut rgba = vec![0u8; (w as usize) * (h as usize) * 4];
4371 na_mpeg2_decoder::frame_to_rgba_bt601_limited_scaled(
4372 &f, &mut rgba, w as usize, h as usize,
4373 );
4374 first = Some(Arc::new(RgbaImage {
4375 width: w,
4376 height: h,
4377 center_x: 0,
4378 center_y: 0,
4379 rgba,
4380 }));
4381 }
4382 })?;
4383 }
4384 first.ok_or_else(|| anyhow!("mpeg2 preview frame missing: {}", path.display()))
4385 }
4386}
4387
4388fn decode_omv_preview_frame(path: &Path) -> Result<Arc<RgbaImage>> {
4389 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
4390 {
4391 let bytes = read_movie_bytes(path)?;
4392 let asset = decode_omv_asset_from_bytes(path, bytes)?;
4393 return asset
4394 .frames
4395 .into_iter()
4396 .next()
4397 .ok_or_else(|| anyhow!("omv preview frame missing: {}", path.display()));
4398 }
4399
4400 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
4401 {
4402 let omv = siglus_assets::omv::OmvFile::open(path)
4403 .with_context(|| format!("open OMV index: {}", path.display()))?;
4404 let reader = omv
4405 .open_embedded_ogg_reader(path)
4406 .with_context(|| format!("open embedded Ogg stream: {}", path.display()))?;
4407 let mut stream = siglus_omv_decoder::TheoraVideoStream::open(reader)
4408 .with_context(|| format!("open streaming Theora decoder: {}", path.display()))?;
4409 let vinfo = stream.info();
4410 let packed = stream
4411 .read_video_frame()?
4412 .ok_or_else(|| anyhow!("omv preview frame missing: {}", path.display()))?;
4413 let display_h = omv.header.display_height as i32;
4414 let width = omv.header.display_width.max(1);
4415 let height = display_h.max(1) as u32;
4416 let (output_width, output_height) = movie_output_dimensions(width, height);
4417 let rgba = convert_omv_frame_to_size(
4418 &packed,
4419 vinfo.frame_width,
4420 vinfo.frame_height,
4421 vinfo.fmt,
4422 width as i32,
4423 display_h,
4424 omv.header.theora_type,
4425 output_width as usize,
4426 output_height as usize,
4427 );
4428 Ok(Arc::new(RgbaImage {
4429 width: output_width,
4430 height: output_height,
4431 center_x: 0,
4432 center_y: 0,
4433 rgba,
4434 }))
4435 }
4436}
4437
4438fn open_wmv_wma_decoder<R: Read + Seek>(
4439 mut reader: R,
4440) -> Result<Option<wmv_decoder::AsfWmaDecoder<R>>> {
4441 let asf = wmv_decoder::asf::AsfFile::open(&mut reader).context("probe ASF audio streams")?;
4442 if asf.audio_streams.is_empty() {
4443 return Ok(None);
4444 }
4445 if !asf
4446 .audio_streams
4447 .iter()
4448 .any(|stream| matches!(stream.format_tag, 0x0160..=0x0162))
4449 {
4450 let tags = asf
4451 .audio_streams
4452 .iter()
4453 .map(|stream| format!("{:#06x}", stream.format_tag))
4454 .collect::<Vec<_>>()
4455 .join(", ");
4456 bail!("unsupported ASF/WMA audio format tag(s): {tags}");
4457 }
4458 reader.seek(SeekFrom::Start(0))?;
4459 let decoder = wmv_decoder::AsfWmaDecoder::open(reader).context("open ASF/WMA decoder")?;
4460 Ok(Some(decoder))
4461}
4462
4463fn decode_wmv_audio_full_from_reader<R: Read + Seek>(
4464 reader: R,
4465 cancel: &AtomicBool,
4466 timeline_origin_ms: u64,
4467) -> Result<Option<MovieAudio>> {
4468 let Some(mut decoder) = open_wmv_wma_decoder(reader)? else {
4469 return Ok(None);
4470 };
4471 let channels = decoder.channels();
4472 let sample_rate = decoder.sample_rate();
4473 if channels == 0 || sample_rate == 0 {
4474 return Ok(None);
4475 }
4476
4477 let mut samples = Vec::<i16>::new();
4478 let mut first_pts_ms = None::<u64>;
4479 while !cancel.load(Ordering::Acquire) {
4480 let decoded = decoder.next_frame()?;
4481 let Some(decoded) = decoded else {
4482 break;
4483 };
4484 first_pts_ms.get_or_insert(decoded.pts_ms as u64);
4485 samples.extend(decoded.frame.samples.iter().copied().map(f32_to_i16_sample));
4486 }
4487 if cancel.load(Ordering::Acquire) || samples.is_empty() {
4488 return Ok(None);
4489 }
4490 let first_audio_pts_ms = first_pts_ms.unwrap_or(timeline_origin_ms);
4491 let (initial_silence_frames, source_skip_frames) =
4492 wmv_audio_alignment_frames(first_audio_pts_ms, timeline_origin_ms, sample_rate);
4493 let source_skip_samples = source_skip_frames.saturating_mul(channels as usize);
4494 if source_skip_samples >= samples.len() {
4495 return Ok(None);
4496 }
4497 if source_skip_samples > 0 {
4498 samples.drain(..source_skip_samples);
4499 }
4500 if initial_silence_frames > 0 {
4501 let mut timeline_samples = Vec::with_capacity(
4502 initial_silence_frames
4503 .saturating_mul(channels as usize)
4504 .saturating_add(samples.len()),
4505 );
4506 timeline_samples.resize(initial_silence_frames.saturating_mul(channels as usize), 0);
4507 timeline_samples.extend_from_slice(&samples);
4508 samples = timeline_samples;
4509 }
4510 let frames = samples.len() as u64 / channels as u64;
4511 let duration_ms = Some(
4512 ((frames as f64) * 1000.0 / sample_rate as f64)
4513 .round()
4514 .max(1.0) as u64,
4515 );
4516 Ok(Some(MovieAudio {
4517 samples: Arc::new(samples),
4518 mpeg_stream: None,
4519 wmv_stream: None,
4520 channels,
4521 sample_rate,
4522 start_ms: 0,
4523 duration_ms,
4524 }))
4525}
4526
4527fn decode_wmv_audio_for_path(path: &Path, cancel: &AtomicBool) -> Result<Option<MovieAudio>> {
4528 let timeline_origin_ms = probe_wmv_first_video_pts_ms(path)?.unwrap_or(0);
4534 if cancel.load(Ordering::Acquire) {
4535 return Ok(None);
4536 }
4537
4538 let file = fs::File::open(path)
4539 .with_context(|| format!("open WMV audio source: {}", path.display()))?;
4540 let Some(mut decoder) = open_wmv_wma_decoder(BufReader::new(file))? else {
4541 return Ok(None);
4542 };
4543 let channels = decoder.channels();
4544 let sample_rate = decoder.sample_rate();
4545 if channels == 0 || sample_rate == 0 {
4546 return Ok(None);
4547 }
4548
4549 let first = decoder
4554 .next_frame()
4555 .with_context(|| format!("probe first WMA frame: {}", path.display()))?;
4556 if cancel.load(Ordering::Acquire) {
4557 return Ok(None);
4558 }
4559 let Some(first) = first else {
4560 return Ok(None);
4561 };
4562 let first_audio_pts_ms = first.pts_ms as u64;
4563 let (initial_silence_frames, source_skip_frames) =
4564 wmv_audio_alignment_frames(first_audio_pts_ms, timeline_origin_ms, sample_rate);
4565
4566 let duration_ms = decoder
4570 .duration_ms()
4571 .and_then(|duration| wmv_rebase_duration_ms(duration, timeline_origin_ms));
4572
4573 let Some(duration_ms) = duration_ms.filter(|duration| *duration > 0) else {
4574 let file = fs::File::open(path)
4578 .with_context(|| format!("reopen WMV audio fallback: {}", path.display()))?;
4579 return decode_wmv_audio_full_from_reader(BufReader::new(file), cancel, timeline_origin_ms)
4580 .with_context(|| format!("decode WMA audio fallback: {}", path.display()));
4581 };
4582
4583 let num_frames = ((duration_ms as u128) * sample_rate as u128)
4584 .div_ceil(1000)
4585 .max(1)
4586 .min(usize::MAX as u128) as usize;
4587
4588 Ok(Some(MovieAudio {
4589 samples: Arc::new(Vec::new()),
4590 mpeg_stream: None,
4591 wmv_stream: Some(WmvStreamAudio {
4592 path: Arc::new(path.to_path_buf()),
4593 num_frames,
4594 initial_silence_frames,
4595 source_skip_frames,
4596 }),
4597 channels,
4598 sample_rate,
4599 start_ms: 0,
4600 duration_ms: Some(duration_ms),
4601 }))
4602}
4603
4604fn decode_mpeg2_audio_for_path(path: &Path, cancel: &AtomicBool) -> Result<Option<MovieAudio>> {
4605 if let Some(audio) = quick_probe_mpeg2_stream_audio(path, cancel)? {
4606 return Ok(Some(audio));
4607 }
4608
4609 decode_mpeg2_audio_full_probe_or_static(path, cancel)
4613}
4614
4615fn quick_probe_mpeg2_stream_audio(path: &Path, cancel: &AtomicBool) -> Result<Option<MovieAudio>> {
4616 let mut file =
4617 fs::File::open(path).with_context(|| format!("open movie file: {}", path.display()))?;
4618 let file_len = file
4619 .metadata()
4620 .with_context(|| format!("stat movie file: {}", path.display()))?
4621 .len();
4622 if file_len == 0 {
4623 return Ok(None);
4624 }
4625
4626 let mut head_probe = na_mpeg2_decoder::MpegAudioProbePipeline::new();
4627 let mut buf = vec![0u8; MPEG2_STREAM_CHUNK_BYTES];
4628 let mut head_read = 0u64;
4629 let head_limit = file_len.min(MPEG_AUDIO_HEAD_PROBE_MAX_BYTES);
4630 let mut prefix = Vec::new();
4631 while head_read < head_limit {
4632 if cancel.load(Ordering::Acquire) {
4633 return Ok(None);
4634 }
4635 let want = (head_limit - head_read).min(buf.len() as u64) as usize;
4636 let n = file
4637 .read(&mut buf[..want])
4638 .with_context(|| format!("probe movie audio head: {}", path.display()))?;
4639 if n == 0 {
4640 break;
4641 }
4642 if prefix.len() < 188 * 3 {
4643 let take = (188 * 3 - prefix.len()).min(n);
4644 prefix.extend_from_slice(&buf[..take]);
4645 }
4646 head_probe.push(&buf[..n], None);
4647 head_read = head_read.saturating_add(n as u64);
4648 if head_probe
4649 .stream_info()
4650 .is_some_and(|info| info.first_video_pts_90k.is_some())
4651 {
4652 break;
4653 }
4654 }
4655 let Some(head) = head_probe.stream_info() else {
4656 return Ok(None);
4657 };
4658 if head.sample_rate == 0 || head.channels == 0 {
4659 return Ok(None);
4660 }
4661 let transport_stream = is_transport_stream_prefix(&prefix);
4662
4663 let mut tail_window = MPEG_AUDIO_TAIL_PROBE_INITIAL_BYTES.min(file_len);
4664 let tail = loop {
4665 if cancel.load(Ordering::Acquire) {
4666 return Ok(None);
4667 }
4668 let mut start = file_len.saturating_sub(tail_window);
4669 if transport_stream {
4670 start -= start % 188;
4671 }
4672 file.seek(SeekFrom::Start(start))
4673 .with_context(|| format!("seek movie audio tail: {}", path.display()))?;
4674 let mut tail_bytes = Vec::with_capacity((file_len - start).min(usize::MAX as u64) as usize);
4675 file.read_to_end(&mut tail_bytes)
4676 .with_context(|| format!("read movie audio tail: {}", path.display()))?;
4677 let probe_bytes = if transport_stream {
4678 tail_bytes.as_slice()
4679 } else if let Some(pack) = find_byte_pattern(&tail_bytes, b"\0\0\x01\xba") {
4680 &tail_bytes[pack..]
4681 } else {
4682 tail_bytes.as_slice()
4683 };
4684 let mut tail_probe = na_mpeg2_decoder::MpegAudioTailProbePipeline::new();
4685 tail_probe.push(probe_bytes);
4686 if let Some(info) = tail_probe
4687 .finish()
4688 .filter(|info| info.sample_rate == head.sample_rate && info.channels == head.channels)
4689 {
4690 break Some(info);
4691 }
4692 if tail_window >= file_len || tail_window >= MPEG_AUDIO_TAIL_PROBE_MAX_BYTES {
4693 break None;
4694 }
4695 tail_window = tail_window
4696 .saturating_mul(2)
4697 .min(file_len)
4698 .min(MPEG_AUDIO_TAIL_PROBE_MAX_BYTES);
4699 };
4700 let Some(tail) = tail else {
4701 return Ok(None);
4702 };
4703
4704 let origin_pts_90k = head
4705 .first_video_pts_90k
4706 .map(|video| earlier_mpeg_pts_90k(video, head.first_audio_pts_90k))
4707 .unwrap_or(head.first_audio_pts_90k);
4708 let delay_ticks = mpeg_pts_delta_90k(head.first_audio_pts_90k, origin_pts_90k).unwrap_or(0);
4709 let before_tail_ticks = match mpeg_pts_delta_90k(tail.anchor_pts_90k, head.first_audio_pts_90k)
4710 {
4711 Some(value) => value,
4712 None => return Ok(None),
4713 };
4714 let delay_frames = pts90k_ticks_to_audio_frames(delay_ticks, head.sample_rate);
4715 let before_tail_frames = pts90k_ticks_to_audio_frames(before_tail_ticks, head.sample_rate);
4716 let num_frames = delay_frames
4717 .saturating_add(before_tail_frames)
4718 .saturating_add(tail.output_frames);
4719 if num_frames == 0 {
4720 return Ok(None);
4721 }
4722 let tail_ticks = audio_frames_to_pts90k_ticks(tail.output_frames, head.sample_rate);
4723 let audio_end_pts_90k = tail.anchor_pts_90k.saturating_add(tail_ticks) & ((1i64 << 33) - 1);
4724 let duration_ms = Some(
4725 ((num_frames as u128).saturating_mul(1000) / head.sample_rate as u128).min(u64::MAX as u128)
4726 as u64,
4727 );
4728
4729 Ok(Some(MovieAudio {
4730 samples: Arc::new(Vec::new()),
4731 mpeg_stream: Some(MpegStreamAudio {
4732 path: Arc::new(path.to_path_buf()),
4733 num_frames,
4734 first_audio_pts_ms: pts90k_to_ms(head.first_audio_pts_90k),
4735 first_video_pts_ms: head.first_video_pts_90k.map(pts90k_to_ms),
4736 first_audio_pts_90k: head.first_audio_pts_90k,
4737 first_video_pts_90k: head.first_video_pts_90k,
4738 audio_end_pts_90k,
4739 file_len,
4740 transport_stream,
4741 }),
4742 wmv_stream: None,
4743 channels: head.channels,
4744 sample_rate: head.sample_rate,
4745 start_ms: 0,
4746 duration_ms,
4747 }))
4748}
4749
4750fn decode_mpeg2_audio_full_probe_or_static(
4751 path: &Path,
4752 cancel: &AtomicBool,
4753) -> Result<Option<MovieAudio>> {
4754 let mut file =
4755 fs::File::open(path).with_context(|| format!("open movie file: {}", path.display()))?;
4756 let file_len = file.metadata().map(|m| m.len()).unwrap_or(0);
4757 let mut probe = na_mpeg2_decoder::MpegAudioProbePipeline::new();
4758 let mut buf = vec![0u8; MPEG2_STREAM_CHUNK_BYTES];
4759 let mut prefix = Vec::new();
4760
4761 loop {
4762 if cancel.load(Ordering::Acquire) {
4763 return Ok(None);
4764 }
4765 let n = file
4766 .read(&mut buf)
4767 .with_context(|| format!("probe movie audio stream: {}", path.display()))?;
4768 if n == 0 {
4769 break;
4770 }
4771 if prefix.len() < 188 * 3 {
4772 let take = (188 * 3 - prefix.len()).min(n);
4773 prefix.extend_from_slice(&buf[..take]);
4774 }
4775 probe.push(&buf[..n], None);
4776 }
4777
4778 if let Some(info) = probe.finish() {
4779 let duration_ms = Some(
4780 ((info.num_frames as u128).saturating_mul(1000) / info.sample_rate as u128)
4781 .min(u64::MAX as u128) as u64,
4782 );
4783 let audio_duration_ticks = audio_frames_to_pts90k_ticks(
4784 info.num_frames.saturating_sub(pts90k_ticks_to_audio_frames(
4785 mpeg_pts_delta_90k(
4786 info.first_audio_pts_90k,
4787 info.first_video_pts_90k
4788 .map(|video| earlier_mpeg_pts_90k(video, info.first_audio_pts_90k))
4789 .unwrap_or(info.first_audio_pts_90k),
4790 )
4791 .unwrap_or(0),
4792 info.sample_rate,
4793 )),
4794 info.sample_rate,
4795 );
4796 return Ok(Some(MovieAudio {
4797 samples: Arc::new(Vec::new()),
4798 mpeg_stream: Some(MpegStreamAudio {
4799 path: Arc::new(path.to_path_buf()),
4800 num_frames: info.num_frames,
4801 first_audio_pts_ms: info.first_audio_pts_ms,
4802 first_video_pts_ms: info.first_video_pts_ms,
4803 first_audio_pts_90k: info.first_audio_pts_90k,
4804 first_video_pts_90k: info.first_video_pts_90k,
4805 audio_end_pts_90k: info
4806 .first_audio_pts_90k
4807 .saturating_add(audio_duration_ticks),
4808 file_len,
4809 transport_stream: is_transport_stream_prefix(&prefix),
4810 }),
4811 wmv_stream: None,
4812 channels: info.channels,
4813 sample_rate: info.sample_rate,
4814 start_ms: 0,
4815 duration_ms,
4816 }));
4817 }
4818
4819 decode_mpeg2_audio_static_fallback(path, cancel)
4820}
4821
4822fn is_transport_stream_prefix(prefix: &[u8]) -> bool {
4823 prefix.len() >= 188 * 3 && prefix[0] == 0x47 && prefix[188] == 0x47 && prefix[376] == 0x47
4824}
4825
4826fn find_byte_pattern(haystack: &[u8], needle: &[u8]) -> Option<usize> {
4827 if needle.is_empty() || haystack.len() < needle.len() {
4828 return None;
4829 }
4830 haystack
4831 .windows(needle.len())
4832 .position(|window| window == needle)
4833}
4834
4835fn earlier_mpeg_pts_90k(lhs: i64, rhs: i64) -> i64 {
4836 if mpeg_pts_delta_90k(lhs, rhs).is_some() {
4837 rhs
4838 } else {
4839 lhs
4840 }
4841}
4842
4843fn mpeg_pts_delta_90k(later: i64, earlier: i64) -> Option<i64> {
4844 const PTS_WRAP: i64 = 1i64 << 33;
4845 let mut delta = later.saturating_sub(earlier);
4846 if delta < 0 {
4847 delta = delta.saturating_add(PTS_WRAP);
4848 }
4849 (delta <= PTS_WRAP / 2).then_some(delta)
4852}
4853
4854fn pts90k_ticks_to_audio_frames(ticks: i64, sample_rate: u32) -> usize {
4855 if ticks <= 0 || sample_rate == 0 {
4856 return 0;
4857 }
4858 (((ticks as i128) * (sample_rate as i128) + 45_000) / 90_000).clamp(0, usize::MAX as i128)
4859 as usize
4860}
4861
4862fn audio_frames_to_pts90k_ticks(frames: usize, sample_rate: u32) -> i64 {
4863 if frames == 0 || sample_rate == 0 {
4864 return 0;
4865 }
4866 (((frames as i128) * 90_000 + sample_rate as i128 / 2) / sample_rate as i128)
4867 .clamp(0, i64::MAX as i128) as i64
4868}
4869
4870fn pts90k_to_ms(pts: i64) -> i64 {
4871 ((pts as i128) * 1000 / 90_000).clamp(i64::MIN as i128, i64::MAX as i128) as i64
4872}
4873
4874fn decode_mpeg2_audio_static_fallback(
4875 path: &Path,
4876 cancel: &AtomicBool,
4877) -> Result<Option<MovieAudio>> {
4878 let mut chunks = Vec::<na_mpeg2_decoder::MpegAudioF32>::new();
4879 let mut file =
4880 fs::File::open(path).with_context(|| format!("open movie file: {}", path.display()))?;
4881 let mut pipeline = na_mpeg2_decoder::MpegAudioPipeline::new();
4882 let mut buf = vec![0u8; MPEG2_STREAM_CHUNK_BYTES];
4883
4884 loop {
4885 if cancel.load(Ordering::Acquire) {
4886 return Ok(None);
4887 }
4888 let n = file
4889 .read(&mut buf)
4890 .with_context(|| format!("read movie audio stream: {}", path.display()))?;
4891 if n == 0 {
4892 break;
4893 }
4894 pipeline
4895 .push_with(&buf[..n], None, |audio| chunks.push(audio))
4896 .context("mpeg2 audio-only decode")?;
4897 }
4898 if cancel.load(Ordering::Acquire) {
4899 return Ok(None);
4900 }
4901 pipeline
4902 .flush_with(|audio| chunks.push(audio))
4903 .context("mpeg2 audio-only flush")?;
4904
4905 build_mpeg2_audio_timeline(path, chunks, pipeline.first_video_pts_ms(), cancel)
4906}
4907
4908fn build_mpeg2_audio_timeline(
4909 path: &Path,
4910 chunks: Vec<na_mpeg2_decoder::MpegAudioF32>,
4911 first_video_pts_ms: Option<i64>,
4912 cancel: &AtomicBool,
4913) -> Result<Option<MovieAudio>> {
4914 let Some(first) = chunks
4915 .iter()
4916 .find(|chunk| chunk.channels > 0 && chunk.sample_rate > 0 && !chunk.samples.is_empty())
4917 else {
4918 return Ok(None);
4919 };
4920
4921 let channels = first.channels;
4922 let sample_rate = first.sample_rate;
4923 let first_audio_pts_ms = chunks
4924 .iter()
4925 .find(|chunk| chunk.channels > 0 && chunk.sample_rate > 0 && !chunk.samples.is_empty())
4926 .map(|chunk| chunk.pts_ms)
4927 .unwrap_or(0);
4928 let timeline_origin_ms = match first_video_pts_ms {
4929 Some(video_pts) => video_pts.min(first_audio_pts_ms),
4930 None => first_audio_pts_ms,
4931 };
4932
4933 let initial_delay_ms = first_audio_pts_ms.saturating_sub(timeline_origin_ms).max(0);
4943 let initial_delay_frames = ((initial_delay_ms as i128) * (sample_rate as i128) / 1000)
4944 .clamp(0, usize::MAX as i128) as usize;
4945 let mut samples = vec![0i16; initial_delay_frames.saturating_mul(channels as usize)];
4946 let mut decoded_frames = 0usize;
4947 let mut discontinuity_count = 0usize;
4948 let mut max_abs_drift_frames = 0usize;
4949 let tolerance_frames = ((sample_rate as u64) * 5 / 1000).max(1) as usize;
4950
4951 for chunk in chunks {
4952 if cancel.load(Ordering::Acquire) {
4953 return Ok(None);
4954 }
4955 if chunk.channels == 0 || chunk.sample_rate == 0 || chunk.samples.is_empty() {
4956 continue;
4957 }
4958 let converted = convert_movie_audio_chunk(
4959 &chunk.samples,
4960 chunk.channels,
4961 chunk.sample_rate,
4962 channels,
4963 sample_rate,
4964 );
4965 if converted.is_empty() {
4966 continue;
4967 }
4968
4969 let relative_pts_ms = chunk.pts_ms.saturating_sub(first_audio_pts_ms);
4973 if relative_pts_ms >= 0 {
4974 let pts_frames = ((relative_pts_ms as i128) * (sample_rate as i128) / 1000)
4975 .clamp(0, usize::MAX as i128) as usize;
4976 let abs_drift = pts_frames.abs_diff(decoded_frames);
4977 max_abs_drift_frames = max_abs_drift_frames.max(abs_drift);
4978 if abs_drift > tolerance_frames {
4979 discontinuity_count = discontinuity_count.saturating_add(1);
4980 }
4981 }
4982
4983 decoded_frames = decoded_frames.saturating_add(converted.len() / channels as usize);
4984 samples.extend_from_slice(&converted);
4985 }
4986
4987 if (env_is_set!("SG_MOVIE_TRACE") || env_is_set!("SG_DEBUG")) && discontinuity_count > 0 {
4988 eprintln!(
4989 "[SG_DEBUG][MOV] audio_pts.contiguous path={} anchors_over_tolerance={} max_drift_frames={} max_drift_ms={}",
4990 path.display(),
4991 discontinuity_count,
4992 max_abs_drift_frames,
4993 (max_abs_drift_frames as u64).saturating_mul(1000) / sample_rate as u64
4994 );
4995 }
4996
4997 if samples.is_empty() {
4998 return Ok(None);
4999 }
5000 let frames_len = samples.len() as u64 / channels as u64;
5001 let duration_ms = Some(((frames_len as f64) * 1000.0 / sample_rate as f64).round() as u64);
5002 Ok(Some(MovieAudio {
5003 samples: Arc::new(samples),
5004 mpeg_stream: None,
5005 wmv_stream: None,
5006 channels,
5007 sample_rate,
5008 start_ms: 0,
5009 duration_ms,
5010 }))
5011}
5012
5013fn convert_movie_audio_chunk(
5014 samples: &[f32],
5015 src_channels: u16,
5016 src_sample_rate: u32,
5017 dst_channels: u16,
5018 dst_sample_rate: u32,
5019) -> Vec<i16> {
5020 let src_channels_usize = src_channels as usize;
5021 let dst_channels_usize = dst_channels as usize;
5022 if src_channels_usize == 0
5023 || dst_channels_usize == 0
5024 || src_sample_rate == 0
5025 || dst_sample_rate == 0
5026 {
5027 return Vec::new();
5028 }
5029 let src_frames = samples.len() / src_channels_usize;
5030 if src_frames == 0 {
5031 return Vec::new();
5032 }
5033
5034 if src_channels == dst_channels && src_sample_rate == dst_sample_rate {
5035 return samples.iter().copied().map(f32_to_i16_sample).collect();
5036 }
5037
5038 let dst_frames = ((src_frames as u128) * (dst_sample_rate as u128) / (src_sample_rate as u128))
5039 .max(1) as usize;
5040 let mut out = Vec::with_capacity(dst_frames.saturating_mul(dst_channels_usize));
5041
5042 for dst_frame in 0..dst_frames {
5043 let src_pos_num = (dst_frame as u128) * (src_sample_rate as u128);
5044 let src_index = (src_pos_num / dst_sample_rate as u128) as usize;
5045 let src_index = src_index.min(src_frames - 1);
5046 let next_index = src_index.saturating_add(1).min(src_frames - 1);
5047 let frac_num = (src_pos_num % dst_sample_rate as u128) as f32;
5048 let frac = frac_num / dst_sample_rate as f32;
5049
5050 for dst_channel in 0..dst_channels_usize {
5051 let sample_at = |frame: usize| -> f32 {
5052 if dst_channels_usize == 1 && src_channels_usize > 1 {
5053 let base = frame * src_channels_usize;
5054 let sum: f32 = samples[base..base + src_channels_usize]
5055 .iter()
5056 .copied()
5057 .sum();
5058 sum / src_channels_usize as f32
5059 } else if src_channels_usize == 1 {
5060 samples[frame]
5061 } else {
5062 let src_channel = dst_channel.min(src_channels_usize - 1);
5063 samples[frame * src_channels_usize + src_channel]
5064 }
5065 };
5066 let a = sample_at(src_index);
5067 let b = sample_at(next_index);
5068 out.push(f32_to_i16_sample(a + (b - a) * frac));
5069 }
5070 }
5071 out
5072}
5073
5074fn decode_wmv_asset(path: &Path) -> Result<MovieAsset> {
5075 let info = probe_wmv_movie_info(path)?;
5076 let frame = decode_wmv_preview_frame(path)?;
5077 Ok(MovieAsset {
5078 info: MovieInfo {
5079 width: info.width.or(Some(frame.width)),
5080 height: info.height.or(Some(frame.height)),
5081 decoded_frames: Some(1),
5082 ..info
5083 },
5084 frames: vec![frame],
5085 audio: None,
5086 })
5087}
5088
5089fn decode_mpeg2_asset(path: &Path) -> Result<MovieAsset> {
5090 let prefix = read_file_prefix(path, MPEG2_HEADER_PROBE_BYTES)
5091 .with_context(|| format!("read movie header: {}", path.display()))?;
5092 let mut width = None;
5093 let mut height = None;
5094 let mut fps = None;
5095 if let Some(h) = siglus_assets::mpeg2::find_sequence_header(&prefix) {
5096 width = Some(h.width as u32);
5097 height = Some(h.height as u32);
5098 fps = siglus_assets::mpeg2::fps_from_frame_rate_code(h.frame_rate_code);
5099 }
5100 let frame = decode_mpeg2_preview_frame(path)?;
5101 let info = MovieInfo {
5102 path: path.to_path_buf(),
5103 width: width.or(Some(frame.width)),
5104 height: height.or(Some(frame.height)),
5105 fps,
5106 decoded_frames: Some(1),
5107 audio_duration_ms: None,
5108 };
5109 Ok(MovieAsset {
5110 info,
5111 frames: vec![frame],
5112 audio: None,
5113 })
5114}
5115
5116fn f32_to_i16_sample(s: f32) -> i16 {
5117 let clamped = if s.is_nan() { -1.0 } else { s.clamp(-1.0, 1.0) };
5118 (clamped * 32767.0).round() as i16
5119}
5120
5121fn decode_omv_asset(path: &Path) -> Result<MovieAsset> {
5122 let omv = siglus_assets::omv::OmvFile::open(path).ok();
5123 let frame = decode_omv_preview_frame(path)?;
5124 let fps = omv.as_ref().and_then(|m| {
5125 if m.header.frame_time_us != 0 {
5126 Some(1_000_000.0 / (m.header.frame_time_us as f32))
5127 } else {
5128 None
5129 }
5130 });
5131 let decoded_frames = omv
5132 .as_ref()
5133 .and_then(|m| {
5134 (m.header.packet_count_hint > 0).then_some(m.header.packet_count_hint as usize)
5135 })
5136 .or(Some(1));
5137 let audio_duration_ms = decoded_frames.and_then(|frames| {
5138 omv_frame_duration_ms(omv.as_ref().map(|m| &m.header), fps)
5139 .map(|ms| ((frames as f64) * ms).round().max(1.0) as u64)
5140 });
5141 let info = MovieInfo {
5142 path: path.to_path_buf(),
5143 width: Some(frame.width),
5144 height: Some(frame.height),
5145 fps,
5146 decoded_frames,
5147 audio_duration_ms,
5148 };
5149 Ok(MovieAsset {
5150 info,
5151 frames: vec![frame],
5152 audio: None,
5153 })
5154}
5155
5156fn decode_omv_audio(tf: &mut siglus_omv_decoder::TheoraFile) -> Result<Option<MovieAudio>> {
5157 if !tf.has_audio() {
5158 return Ok(None);
5159 }
5160 let Some((channels, sample_rate)) = tf.audio_info() else {
5161 return Ok(None);
5162 };
5163 if channels <= 0 || sample_rate <= 0 {
5164 return Ok(None);
5165 }
5166 let channels_u16 = channels as u16;
5167 let sample_rate_u32 = sample_rate as u32;
5168
5169 let mut samples: Vec<f32> = Vec::new();
5170 let mut buf = vec![0.0f32; (4096usize).saturating_mul(channels as usize)];
5171 loop {
5172 let read = tf.read_audio_samples(&mut buf)?;
5173 if read == 0 {
5174 break;
5175 }
5176 samples.extend_from_slice(&buf[..read]);
5177 }
5178
5179 if samples.is_empty() {
5180 return Ok(None);
5181 }
5182 let mut samples_i16: Vec<i16> = Vec::with_capacity(samples.len());
5183 for &s in &samples {
5184 let clamped = if s.is_nan() { -1.0 } else { s.clamp(-1.0, 1.0) };
5185 let v = (clamped * 32767.0).round() as i16;
5186 samples_i16.push(v);
5187 }
5188 let frames = (samples_i16.len() as u64) / (channels_u16 as u64);
5189 let duration_ms = if sample_rate_u32 > 0 {
5190 Some(((frames as f64) * 1000.0 / (sample_rate_u32 as f64)).round() as u64)
5191 } else {
5192 None
5193 };
5194
5195 Ok(Some(MovieAudio {
5196 samples: Arc::new(samples_i16),
5197 mpeg_stream: None,
5198 wmv_stream: None,
5199 channels: channels_u16,
5200 sample_rate: sample_rate_u32,
5201 start_ms: 0,
5202 duration_ms,
5203 }))
5204}
5205
5206fn encode_wav_i16_interleaved(samples: &[i16], channels: u16, sample_rate: u32) -> Vec<u8> {
5207 let bytes_per_sample = 2u16;
5208 let block_align = channels.saturating_mul(bytes_per_sample);
5209 let byte_rate = (sample_rate as u64).saturating_mul(block_align as u64) as u32;
5210 let data_bytes = samples.len().saturating_mul(bytes_per_sample as usize) as u32;
5211 let riff_size = 36u32.saturating_add(data_bytes);
5212
5213 let mut out = Vec::with_capacity((data_bytes as usize) + 44);
5214 out.extend_from_slice(b"RIFF");
5215 out.extend_from_slice(&riff_size.to_le_bytes());
5216 out.extend_from_slice(b"WAVE");
5217 out.extend_from_slice(b"fmt ");
5218 out.extend_from_slice(&16u32.to_le_bytes());
5219 out.extend_from_slice(&1u16.to_le_bytes());
5220 out.extend_from_slice(&channels.to_le_bytes());
5221 out.extend_from_slice(&sample_rate.to_le_bytes());
5222 out.extend_from_slice(&byte_rate.to_le_bytes());
5223 out.extend_from_slice(&block_align.to_le_bytes());
5224 out.extend_from_slice(&16u16.to_le_bytes());
5225 out.extend_from_slice(b"data");
5226 out.extend_from_slice(&data_bytes.to_le_bytes());
5227
5228 for &s in samples {
5229 out.extend_from_slice(&s.to_le_bytes());
5230 }
5231 out
5232}
5233
5234fn convert_omv_frame(
5235 data: &[u8],
5236 frame_width: i32,
5237 frame_height: i32,
5238 fmt: i32,
5239 display_width: i32,
5240 display_height: i32,
5241 theora_type: u32,
5242) -> Vec<u8> {
5243 convert_omv_frame_to_size(
5244 data,
5245 frame_width,
5246 frame_height,
5247 fmt,
5248 display_width,
5249 display_height,
5250 theora_type,
5251 display_width.max(1) as usize,
5252 display_height.max(1) as usize,
5253 )
5254}
5255
5256fn convert_omv_frame_to_size(
5257 data: &[u8],
5258 frame_width: i32,
5259 frame_height: i32,
5260 fmt: i32,
5261 display_width: i32,
5262 display_height: i32,
5263 theora_type: u32,
5264 output_width: usize,
5265 output_height: usize,
5266) -> Vec<u8> {
5267 let (uv_w, _uv_h, y_plane_len, u_plane_len, _v_plane_len) =
5274 omv_plane_layout(frame_width, frame_height, fmt);
5275 let planes = PackedOmvPlanes {
5276 data,
5277 offsets: [0, y_plane_len, y_plane_len.saturating_add(u_plane_len)],
5278 widths: [frame_width.max(1) as usize, uv_w, uv_w],
5279 };
5280 convert_omv_planes_to_size(
5281 &planes,
5282 frame_width,
5283 frame_height,
5284 fmt,
5285 display_width,
5286 display_height,
5287 theora_type,
5288 output_width,
5289 output_height,
5290 )
5291}
5292
5293trait OmvPlanes {
5295 fn sample(&self, pli: usize, x: usize, y: usize, default: u8) -> u8;
5297
5298 fn row(&self, pli: usize, y: usize, len: usize) -> Option<&[u8]>;
5301}
5302
5303struct PackedOmvPlanes<'a> {
5305 data: &'a [u8],
5306 offsets: [usize; 3],
5307 widths: [usize; 3],
5308}
5309
5310impl OmvPlanes for PackedOmvPlanes<'_> {
5311 fn sample(&self, pli: usize, x: usize, y: usize, default: u8) -> u8 {
5312 get_plane_sample(
5313 self.data,
5314 self.offsets[pli],
5315 self.widths[pli],
5316 x,
5317 y,
5318 default,
5319 )
5320 }
5321
5322 fn row(&self, pli: usize, y: usize, len: usize) -> Option<&[u8]> {
5323 let width = self.widths[pli];
5324 if width == 0 {
5325 return None;
5326 }
5327 let start = self.offsets[pli].checked_add(y.checked_mul(width)?)?;
5328 self.data.get(start..start.checked_add(len)?)
5329 }
5330}
5331
5332struct DecoderOmvPlanes<'a>(&'a siglus_omv_decoder::YCbCrRef<'a>);
5335
5336impl OmvPlanes for DecoderOmvPlanes<'_> {
5337 fn sample(&self, pli: usize, x: usize, y: usize, default: u8) -> u8 {
5338 let plane = &self.0[pli];
5339 let (width, height) = (plane.width.max(0) as usize, plane.height.max(0) as usize);
5340 if x >= width || y >= height {
5341 return default;
5342 }
5343 let index = plane.data_offset as isize + y as isize * plane.stride as isize + x as isize;
5344 usize::try_from(index)
5345 .ok()
5346 .and_then(|index| plane.data.get(index))
5347 .copied()
5348 .unwrap_or(default)
5349 }
5350
5351 fn row(&self, pli: usize, y: usize, len: usize) -> Option<&[u8]> {
5352 let plane = &self.0[pli];
5353 if len > plane.width.max(0) as usize || y >= plane.height.max(0) as usize {
5354 return None;
5355 }
5356 let start = plane.data_offset as isize + y as isize * plane.stride as isize;
5357 let start = usize::try_from(start).ok()?;
5358 plane.data.get(start..start.checked_add(len)?)
5359 }
5360}
5361
5362fn bilinear_omv_sample(
5363 planes: &impl OmvPlanes,
5364 pli: usize,
5365 x: (usize, usize, u32),
5366 y: (usize, usize, u32),
5367 default: u8,
5368) -> u8 {
5369 let (x0, x1, fx) = x;
5370 let (y0, y1, fy) = y;
5371 let p00 = u64::from(planes.sample(pli, x0, y0, default));
5372 let p10 = u64::from(planes.sample(pli, x1, y0, default));
5373 let p01 = u64::from(planes.sample(pli, x0, y1, default));
5374 let p11 = u64::from(planes.sample(pli, x1, y1, default));
5375 const ONE: u64 = 1 << 16;
5376 let fx = u64::from(fx);
5377 let fy = u64::from(fy);
5378 let top = p00 * (ONE - fx) + p10 * fx;
5379 let bottom = p01 * (ONE - fx) + p11 * fx;
5380 ((top * (ONE - fy) + bottom * fy + (1 << 31)) >> 32) as u8
5381}
5382
5383#[allow(clippy::too_many_arguments)]
5384#[allow(clippy::too_many_arguments)]
5385fn convert_omv_planes_to_size(
5386 planes: &impl OmvPlanes,
5387 frame_width: i32,
5388 frame_height: i32,
5389 fmt: i32,
5390 display_width: i32,
5391 display_height: i32,
5392 theora_type: u32,
5393 output_width: usize,
5394 output_height: usize,
5395) -> Vec<u8> {
5396 let mut rgba = Vec::new();
5397 convert_omv_planes_into(
5398 planes,
5399 frame_width,
5400 frame_height,
5401 fmt,
5402 display_width,
5403 display_height,
5404 theora_type,
5405 output_width,
5406 output_height,
5407 &mut rgba,
5408 );
5409 rgba
5410}
5411
5412#[allow(clippy::too_many_arguments)]
5415fn convert_omv_planes_into(
5416 planes: &impl OmvPlanes,
5417 frame_width: i32,
5418 frame_height: i32,
5419 fmt: i32,
5420 display_width: i32,
5421 display_height: i32,
5422 theora_type: u32,
5423 output_width: usize,
5424 output_height: usize,
5425 rgba: &mut Vec<u8>,
5426) {
5427 let sw = frame_width.max(1) as usize;
5428 let sh = frame_height.max(1) as usize;
5429 let dw = display_width.max(1) as usize;
5430 let dh = display_height.max(1) as usize;
5431 let (uv_w, uv_h) = yuv_plane_size(frame_width, frame_height, fmt);
5432
5433 rgba.resize(output_width.saturating_mul(output_height).saturating_mul(4), 0);
5434 let source_x: Vec<usize> = (0..output_width)
5438 .map(|out_x| out_x * dw / output_width)
5439 .collect();
5440 let row_len = source_x.last().map_or(0, |&x| x + 1);
5441
5442 match (theora_type, fmt) {
5443 (siglus_assets::omv::OMV_THEORA_TYPE_RGB, _) => {
5444 for out_y in 0..output_height {
5445 let y = out_y * dh / output_height;
5446 if let (Some(b_row), Some(g_row), Some(r_row)) = (
5447 planes.row(0, y, row_len),
5448 planes.row(1, y, row_len),
5449 planes.row(2, y, row_len),
5450 ) {
5451 let out_row =
5452 &mut rgba[out_y * output_width * 4..(out_y + 1) * output_width * 4];
5453 for (px, &x) in out_row.chunks_exact_mut(4).zip(&source_x) {
5454 px[0] = r_row[x];
5455 px[1] = g_row[x];
5456 px[2] = b_row[x];
5457 px[3] = 0xff;
5458 }
5459 continue;
5460 }
5461 for out_x in 0..output_width {
5462 let x = out_x * dw / output_width;
5463 let b = planes.sample(0, x, y, 0);
5464 let g = planes.sample(1, x, y, 0);
5465 let r = planes.sample(2, x, y, 0);
5466 let out = (out_y * output_width + out_x) * 4;
5467 rgba[out] = r;
5468 rgba[out + 1] = g;
5469 rgba[out + 2] = b;
5470 rgba[out + 3] = 0xff;
5471 }
5472 }
5473 }
5474 (siglus_assets::omv::OMV_THEORA_TYPE_RGBA, _) => {
5475 let alpha_h = dh.div_ceil(3);
5479 let alpha_h_2 = alpha_h * 2;
5480 for out_y in 0..output_height {
5481 let y = out_y * dh / output_height;
5482 let (a_pli, local_y) = if y < alpha_h {
5483 (0, y)
5484 } else if y < alpha_h_2 {
5485 (1, y - alpha_h)
5486 } else {
5487 (2, y - alpha_h_2)
5488 };
5489 let alpha_y = dh.saturating_add(local_y);
5490 if let (Some(b_row), Some(g_row), Some(r_row), Some(a_row)) = (
5491 planes.row(0, y, row_len),
5492 planes.row(1, y, row_len),
5493 planes.row(2, y, row_len),
5494 planes.row(a_pli, alpha_y, row_len),
5495 ) {
5496 let out_row =
5497 &mut rgba[out_y * output_width * 4..(out_y + 1) * output_width * 4];
5498 for (px, &x) in out_row.chunks_exact_mut(4).zip(&source_x) {
5499 px[0] = r_row[x];
5500 px[1] = g_row[x];
5501 px[2] = b_row[x];
5502 px[3] = a_row[x];
5503 }
5504 continue;
5505 }
5506 for out_x in 0..output_width {
5507 let x = out_x * dw / output_width;
5508 let b = planes.sample(0, x, y, 0);
5509 let g = planes.sample(1, x, y, 0);
5510 let r = planes.sample(2, x, y, 0);
5511 let a = planes.sample(a_pli, x, alpha_y, 0xff);
5512 let out = (out_y * output_width + out_x) * 4;
5513 rgba[out] = r;
5514 rgba[out + 1] = g;
5515 rgba[out + 2] = b;
5516 rgba[out + 3] = a;
5517 }
5518 }
5519 }
5520 (_, siglus_omv_decoder::TH_PF_444) => {
5521 for out_y in 0..output_height {
5525 let y = out_y * dh / output_height;
5526 for out_x in 0..output_width {
5527 let x = out_x * dw / output_width;
5528 let yv = planes.sample(0, x, y, 0) as f32;
5529 let u = planes.sample(1, x, y, 128) as f32 - 128.0;
5530 let v = planes.sample(2, x, y, 128) as f32 - 128.0;
5531 let out = (out_y * output_width + out_x) * 4;
5532 rgba[out] = clamp_f(yv + 1.40200 * v);
5533 rgba[out + 1] = clamp_f(yv - 0.34414 * u - 0.71414 * v);
5534 rgba[out + 2] = clamp_f(yv + 1.77200 * u);
5535 rgba[out + 3] = 0xff;
5536 }
5537 }
5538 }
5539 _ => {
5540 let chroma_x: Vec<_> = (0..dw)
5545 .map(|x| centred_resample_coordinate(x, uv_w, sw))
5546 .collect();
5547 let chroma_y: Vec<_> = (0..dh)
5548 .map(|y| centred_resample_coordinate(y, uv_h, sh))
5549 .collect();
5550
5551 for out_y in 0..output_height {
5552 let y = out_y * dh / output_height;
5553 let y_coord = chroma_y[y];
5554 for out_x in 0..output_width {
5555 let x = out_x * dw / output_width;
5556 let yv = planes.sample(0, x, y, 0) as f32;
5557 let x_coord = chroma_x[x];
5558 let u = bilinear_omv_sample(planes, 1, x_coord, y_coord, 128) as f32 - 128.0;
5559 let v = bilinear_omv_sample(planes, 2, x_coord, y_coord, 128) as f32 - 128.0;
5560
5561 let out = (out_y * output_width + out_x) * 4;
5562 rgba[out] = clamp_f(yv + 1.40200 * v);
5563 rgba[out + 1] = clamp_f(yv - 0.34414 * u - 0.71414 * v);
5564 rgba[out + 2] = clamp_f(yv + 1.77200 * u);
5565 rgba[out + 3] = 0xff;
5566 }
5567 }
5568 }
5569 }
5570}
5571
5572fn get_plane_sample(
5573 data: &[u8],
5574 plane_off: usize,
5575 plane_width: usize,
5576 x: usize,
5577 y: usize,
5578 default: u8,
5579) -> u8 {
5580 if plane_width == 0 {
5581 return default;
5582 }
5583 data.get(
5584 plane_off
5585 .saturating_add(y.saturating_mul(plane_width))
5586 .saturating_add(x),
5587 )
5588 .copied()
5589 .unwrap_or(default)
5590}
5591
5592fn get_bilinear_plane_sample(
5593 data: &[u8],
5594 plane_off: usize,
5595 plane_width: usize,
5596 x: (usize, usize, u32),
5597 y: (usize, usize, u32),
5598 default: u8,
5599) -> u8 {
5600 let (x0, x1, fx) = x;
5601 let (y0, y1, fy) = y;
5602 let p00 = u64::from(get_plane_sample(
5603 data,
5604 plane_off,
5605 plane_width,
5606 x0,
5607 y0,
5608 default,
5609 ));
5610 let p10 = u64::from(get_plane_sample(
5611 data,
5612 plane_off,
5613 plane_width,
5614 x1,
5615 y0,
5616 default,
5617 ));
5618 let p01 = u64::from(get_plane_sample(
5619 data,
5620 plane_off,
5621 plane_width,
5622 x0,
5623 y1,
5624 default,
5625 ));
5626 let p11 = u64::from(get_plane_sample(
5627 data,
5628 plane_off,
5629 plane_width,
5630 x1,
5631 y1,
5632 default,
5633 ));
5634
5635 const ONE: u64 = 1 << 16;
5636 let fx = u64::from(fx);
5637 let fy = u64::from(fy);
5638 let top = p00 * (ONE - fx) + p10 * fx;
5639 let bottom = p01 * (ONE - fx) + p11 * fx;
5640 ((top * (ONE - fy) + bottom * fy + (1 << 31)) >> 32) as u8
5641}
5642
5643fn centred_resample_coordinate(
5644 output_index: usize,
5645 input_len: usize,
5646 output_len: usize,
5647) -> (usize, usize, u32) {
5648 if input_len <= 1 || output_len <= 1 {
5649 return (0, 0, 0);
5650 }
5651
5652 let numerator = (2_i128 * output_index as i128 + 1) * input_len as i128 * (1_i128 << 15);
5656 let mut position = numerator / output_len as i128 - (1_i128 << 15);
5657 let max_position = ((input_len - 1) as i128) << 16;
5658 position = position.clamp(0, max_position);
5659
5660 let i0 = (position >> 16) as usize;
5661 let i1 = (i0 + 1).min(input_len - 1);
5662 let fraction = (position & 0xFFFF) as u32;
5663 (i0, i1, fraction)
5664}
5665
5666fn clamp_f(v: f32) -> u8 {
5667 if v <= 0.0 {
5668 0
5669 } else if v >= 255.0 {
5670 255
5671 } else {
5672 v as u8
5673 }
5674}
5675
5676fn yuv_plane_size(width: i32, height: i32, fmt: i32) -> (usize, usize) {
5677 let w = width.max(1) as usize;
5678 let h = height.max(1) as usize;
5679 match fmt {
5680 siglus_omv_decoder::TH_PF_420 => (w / 2, h / 2),
5681 siglus_omv_decoder::TH_PF_422 => (w / 2, h),
5682 siglus_omv_decoder::TH_PF_444 => (w, h),
5683 _ => (w / 2, h / 2),
5684 }
5685}
5686
5687#[cfg(test)]
5688mod omv_conversion_parity_tests {
5689 use super::convert_omv_frame;
5690
5691 #[test]
5692 fn rgb_uses_omv_display_rect_over_full_coded_planes() {
5693 let b = [1, 2, 3, 90, 4, 5, 6, 91, 7, 8, 9, 92];
5697 let g = [11, 12, 13, 93, 14, 15, 16, 94, 17, 18, 19, 95];
5698 let r = [21, 22, 23, 96, 24, 25, 26, 97, 27, 28, 29, 98];
5699 let mut packed = Vec::new();
5700 packed.extend_from_slice(&b);
5701 packed.extend_from_slice(&g);
5702 packed.extend_from_slice(&r);
5703
5704 let rgba = convert_omv_frame(
5705 &packed,
5706 4,
5707 3,
5708 siglus_omv_decoder::TH_PF_444,
5709 3,
5710 2,
5711 siglus_assets::omv::OMV_THEORA_TYPE_RGB,
5712 );
5713 assert_eq!(
5714 rgba,
5715 vec![
5716 21, 11, 1, 255, 22, 12, 2, 255, 23, 13, 3, 255, 24, 14, 4, 255, 25, 15, 5, 255, 26,
5717 16, 6, 255,
5718 ]
5719 );
5720 }
5721
5722 #[test]
5725 fn decoder_planes_convert_like_packed_planes() {
5726 use super::{DecoderOmvPlanes, convert_omv_frame_to_size, convert_omv_planes_to_size};
5727 let files = [
5728 "/Users/xmoe/Documents/GameData/mov/ef_wind_dust01.omv",
5729 "/Users/xmoe/Documents/GameData/mov/ef_wind04.omv",
5730 "/Users/xmoe/Documents/GameData/mov/ef_fog01.omv",
5731 "/Users/xmoe/Documents/RewriteHF/mov/ny_hf_waterscreen.omv",
5732 ];
5733 for file in files {
5734 let path = std::path::Path::new(file);
5735 if !path.is_file() {
5736 continue;
5737 }
5738 let omv = siglus_assets::omv::OmvFile::open(path).unwrap();
5739 let open = || {
5740 siglus_omv_decoder::TheoraVideoStream::open(
5741 omv.open_embedded_ogg_reader(path).unwrap(),
5742 )
5743 .unwrap()
5744 };
5745 let (mut packed_stream, mut direct_stream) = (open(), open());
5746 let vinfo = packed_stream.info();
5747 let width = omv.header.display_width as i32;
5748 let height = omv.header.display_height as i32;
5749 let theora_type = omv.header.theora_type;
5750 let sizes = [
5751 (width as usize, height as usize),
5752 (width as usize / 2, height as usize / 2),
5753 ];
5754 for frame in 0..40 {
5755 let packed = packed_stream.read_video_frame().unwrap().unwrap();
5756 let direct = direct_stream
5757 .read_video_frame_with(|ycbcr| {
5758 sizes.map(|(w, h)| {
5759 convert_omv_planes_to_size(
5760 &DecoderOmvPlanes(ycbcr),
5761 vinfo.frame_width,
5762 vinfo.frame_height,
5763 vinfo.fmt,
5764 width,
5765 height,
5766 theora_type,
5767 w,
5768 h,
5769 )
5770 })
5771 })
5772 .unwrap()
5773 .unwrap();
5774 for (i, (w, h)) in sizes.into_iter().enumerate() {
5775 let expected = convert_omv_frame_to_size(
5776 &packed,
5777 vinfo.frame_width,
5778 vinfo.frame_height,
5779 vinfo.fmt,
5780 width,
5781 height,
5782 theora_type,
5783 w,
5784 h,
5785 );
5786 assert!(direct[i] == expected, "{file} frame {frame} at {w}x{h}");
5787 }
5788 }
5789 }
5790 }
5791
5792 #[test]
5793 fn rgba_alpha_rows_start_below_omv_display_height() {
5794 let mut y = vec![1u8; 16];
5797 let mut u = vec![2u8; 16];
5798 let mut v = vec![3u8; 16];
5799 y[12..16].copy_from_slice(&[10, 11, 12, 99]);
5800 u[12..16].copy_from_slice(&[20, 21, 22, 99]);
5801 v[12..16].copy_from_slice(&[30, 31, 32, 99]);
5802 let mut packed = Vec::new();
5803 packed.extend_from_slice(&y);
5804 packed.extend_from_slice(&u);
5805 packed.extend_from_slice(&v);
5806
5807 let rgba = convert_omv_frame(
5808 &packed,
5809 4,
5810 4,
5811 siglus_omv_decoder::TH_PF_444,
5812 3,
5813 3,
5814 siglus_assets::omv::OMV_THEORA_TYPE_RGBA,
5815 );
5816 let alpha: Vec<u8> = rgba.as_chunks::<4>().0.iter().map(|px| px[3]).collect();
5817 assert_eq!(alpha, vec![10, 11, 12, 20, 21, 22, 30, 31, 32]);
5818 }
5819}
5820
5821#[cfg(test)]
5822mod mpeg_video_pts_tests {
5823 use std::collections::VecDeque;
5824 use std::sync::atomic::AtomicUsize;
5825 use std::sync::{Arc, mpsc};
5826
5827 use super::{
5828 Mpeg2DecodedFrame, Mpeg2StreamEvent, Mpeg2StreamState, RgbaImage,
5829 mpeg_stream_needs_restart, select_mpeg_stream_frame,
5830 };
5831
5832 fn frame(index: usize, pts_90k: i64) -> Mpeg2DecodedFrame {
5833 Mpeg2DecodedFrame {
5834 frame_idx: index,
5835 pts_90k: Some(pts_90k),
5836 frame: Arc::new(RgbaImage {
5837 width: 1,
5838 height: 1,
5839 center_x: 0,
5840 center_y: 0,
5841 rgba: vec![0, 0, 0, 255],
5842 }),
5843 }
5844 }
5845
5846 fn state(frames: VecDeque<Mpeg2DecodedFrame>, timer_ms: u64) -> Mpeg2StreamState {
5847 let (_tx, rx) = mpsc::channel::<Result<Mpeg2StreamEvent, String>>();
5848 Mpeg2StreamState {
5849 rx,
5850 frames,
5851 width: Some(1),
5852 height: Some(1),
5853 fps: Some(30.0),
5854 decoded_frames: 3,
5855 first_video_pts_90k: Some(0),
5856 last_video_timeline_ms: Some(300),
5857 last_requested_timer_ms: timer_ms,
5858 done: false,
5859 audio: None,
5860 decoded_any_this_poll: false,
5861 request_frames: Arc::new(AtomicUsize::new(0)),
5862 }
5863 }
5864
5865 #[test]
5866 fn lagging_decoder_is_not_restarted_during_continuous_playback() {
5867 let mut state = state(VecDeque::from([frame(30, 90_000)]), 60_000);
5870 for timer in (60_016..80_000).step_by(16) {
5871 assert!(!mpeg_stream_needs_restart(&state, timer));
5872 state.last_requested_timer_ms = timer;
5873 }
5874 }
5875
5876 #[test]
5877 fn future_pts_does_not_turn_continuous_playback_into_a_rewind() {
5878 let state = state(VecDeque::from([frame(1806, 5_418_000)]), 60_000);
5879 assert!(!mpeg_stream_needs_restart(&state, 60_016));
5880 }
5881
5882 #[test]
5883 fn explicit_seek_outside_cache_restarts_once() {
5884 let mut state = state(VecDeque::from([frame(1800, 5_400_000)]), 60_000);
5885 assert!(mpeg_stream_needs_restart(&state, 90_000));
5886 state.last_requested_timer_ms = 90_000;
5887 assert!(!mpeg_stream_needs_restart(&state, 90_016));
5888 assert!(mpeg_stream_needs_restart(&state, 30_000));
5889 }
5890
5891 #[test]
5892 fn rewind_while_seek_is_still_loading_restarts() {
5893 let state = state(VecDeque::new(), 60_000);
5894 assert!(mpeg_stream_needs_restart(&state, 0));
5895 }
5896
5897 #[test]
5898 fn rewind_inside_cache_does_not_restart_decoder() {
5899 let state = state(VecDeque::from([frame(0, 0), frame(9, 27_000)]), 200);
5900 assert!(!mpeg_stream_needs_restart(&state, 100));
5901 }
5902
5903 #[test]
5904 fn frame_selection_uses_pts_instead_of_fixed_fps_index() {
5905 let state = state(
5906 VecDeque::from([frame(0, 0), frame(1, 9_000), frame(2, 27_000)]),
5907 0,
5908 );
5909
5910 let selected =
5913 select_mpeg_stream_frame(&state.frames, &state, 200, Some(6)).expect("selected frame");
5914 assert_eq!(selected.frame_idx, 1);
5915 }
5916}
5917
5918#[cfg(test)]
5919mod mpeg_audio_timeline_tests {
5920 use std::path::Path;
5921 use std::sync::atomic::AtomicBool;
5922
5923 use super::build_mpeg2_audio_timeline;
5924
5925 #[test]
5926 fn pes_pts_discontinuities_do_not_splice_continuous_pcm() {
5927 let frame_samples = 1152usize * 2;
5928 let chunks = vec![
5929 na_mpeg2_decoder::MpegAudioF32 {
5930 pts_ms: 0,
5931 sample_rate: 44_100,
5932 channels: 2,
5933 samples: vec![0.25; frame_samples],
5934 },
5935 na_mpeg2_decoder::MpegAudioF32 {
5936 pts_ms: 26,
5937 sample_rate: 44_100,
5938 channels: 2,
5939 samples: vec![0.5; frame_samples],
5940 },
5941 na_mpeg2_decoder::MpegAudioF32 {
5943 pts_ms: 1_000,
5944 sample_rate: 44_100,
5945 channels: 2,
5946 samples: vec![0.75; frame_samples],
5947 },
5948 ];
5949 let cancel = AtomicBool::new(false);
5950 let audio =
5951 build_mpeg2_audio_timeline(Path::new("synthetic.mpg"), chunks, Some(0), &cancel)
5952 .expect("timeline build")
5953 .expect("audio track");
5954
5955 assert_eq!(audio.channels, 2);
5956 assert_eq!(audio.sample_rate, 44_100);
5957 assert_eq!(audio.samples.len(), frame_samples * 3);
5958 }
5959}
5960
5961#[cfg(test)]
5962mod chroma_resampling_tests {
5963 use super::{centred_resample_coordinate, get_bilinear_plane_sample};
5964
5965 #[test]
5966 fn half_rate_coordinates_are_centre_aligned() {
5967 assert_eq!(centred_resample_coordinate(0, 2, 4), (0, 1, 0));
5968 assert_eq!(centred_resample_coordinate(1, 2, 4), (0, 1, 16_384));
5969 assert_eq!(centred_resample_coordinate(2, 2, 4), (0, 1, 49_152));
5970 assert_eq!(centred_resample_coordinate(3, 2, 4), (1, 1, 0));
5971 }
5972
5973 #[test]
5974 fn bilinear_sample_blends_both_axes() {
5975 let plane = [0_u8, 100, 200, 255];
5976 let half = (0, 1, 32_768);
5977 assert_eq!(
5978 get_bilinear_plane_sample(&plane, 0, 2, half, half, 128),
5979 139
5980 );
5981 }
5982}
5983
5984#[cfg(test)]
5985mod omv_loop_rewind_tests {
5986 use std::collections::VecDeque;
5987 use std::sync::atomic::AtomicUsize;
5988 use std::sync::{Arc, mpsc};
5989
5990 use super::{
5991 OmvStreamEvent, OmvStreamState, RgbaImage, drain_omv_stream_state, omv_frame_for_time,
5992 select_omv_loop_frame, select_stream_frame,
5993 };
5994
5995 fn image(value: u8) -> Arc<RgbaImage> {
5996 Arc::new(RgbaImage {
5997 width: 1,
5998 height: 1,
5999 center_x: 0,
6000 center_y: 0,
6001 rgba: vec![value, value, value, 255],
6002 })
6003 }
6004
6005 fn state() -> OmvStreamState {
6006 let (_tx, rx) = mpsc::channel::<Result<OmvStreamEvent, String>>();
6007 OmvStreamState {
6008 rx,
6009 frames: VecDeque::from([(100, image(100)), (101, image(101))]),
6010 loop_head_frames: VecDeque::from([(0, image(0)), (1, image(1)), (2, image(2))]),
6011 loop_head_bytes: 12,
6012 width: Some(1),
6013 height: Some(1),
6014 fps: Some(30.0),
6015 frame_time_ms: Some(1000.0 / 30.0),
6016 total_frames_hint: Some(102),
6017 total_ms_hint: Some(3_400),
6018 frame_times: None,
6019 decoded_frames: 102,
6020 done: true,
6021 request_frame: Arc::new(AtomicUsize::new(0)),
6022 last_served_frame_idx: 100,
6023 last_effective_timer_ms: 0,
6024 held_frame: None,
6025 }
6026 }
6027
6028 #[test]
6029 fn loop_wrap_selects_cached_head_instead_of_stale_tail() {
6030 let state = state();
6031 let (idx, frame) = select_omv_loop_frame(&state, 0).expect("loop frame");
6032 assert_eq!(idx, 0);
6033 assert_eq!(frame.rgba[0], 0);
6034 }
6035
6036 #[test]
6037 fn request_before_live_queue_returns_its_front_not_its_tail() {
6038 let frames = VecDeque::from([(10, image(10)), (11, image(11))]);
6039 let (idx, _) = select_stream_frame(&frames, 0).expect("front frame");
6040 assert_eq!(idx, 10);
6041 }
6042
6043 #[test]
6044 fn done_does_not_hide_the_queued_rewind_reset() {
6045 let (tx, rx) = mpsc::channel::<Result<OmvStreamEvent, String>>();
6046 let mut state = state();
6047 state.rx = rx;
6048 tx.send(Ok(OmvStreamEvent::Done)).expect("queue done");
6049 tx.send(Ok(OmvStreamEvent::Reset { frame_idx: 0 }))
6050 .expect("queue reset");
6051 tx.send(Ok(OmvStreamEvent::Video {
6052 frame_idx: 0,
6053 frame: image(0),
6054 }))
6055 .expect("queue video");
6056
6057 drain_omv_stream_state(
6058 std::path::Path::new("synthetic.omv"),
6059 &mut state,
6060 Some(0),
6061 false,
6062 true,
6063 )
6064 .expect("drain rewind events");
6065
6066 assert!(!state.done);
6067 assert_eq!(state.frames.front().map(|(idx, _)| *idx), Some(0));
6068 }
6069
6070 #[test]
6071 fn packet_time_boundaries_match_original_lookup() {
6072 let times = [(0, 32), (33, 65), (66, 99)];
6073 assert_eq!(omv_frame_for_time(×, 0), Some(0));
6074 assert_eq!(omv_frame_for_time(×, 32), Some(0));
6075 assert_eq!(omv_frame_for_time(×, 33), Some(1));
6076 assert_eq!(omv_frame_for_time(×, 99), Some(2));
6077 }
6078}
6079
6080#[cfg(test)]
6081mod wmv_movie_adapter_tests {
6082 use std::collections::VecDeque;
6083 use std::sync::Arc;
6084
6085 use super::{
6086 RgbaImage, WmvDecodedFrame, is_wmv_extension, select_wmv_stream_frame,
6087 wmv_audio_alignment_frames, wmv_frame_duration_ms, wmv_rebase_duration_ms,
6088 wmv_yuv_frame_to_rgba,
6089 };
6090
6091 fn frame(index: usize, pts_ms: u64) -> WmvDecodedFrame {
6092 WmvDecodedFrame {
6093 frame_idx: index,
6094 source_pts_ms: pts_ms,
6095 frame: Arc::new(RgbaImage {
6096 width: 1,
6097 height: 1,
6098 center_x: 0,
6099 center_y: 0,
6100 rgba: vec![0, 0, 0, 255],
6101 }),
6102 }
6103 }
6104
6105 #[test]
6106 fn source_pts_are_rebased_to_movie_time_zero() {
6107 let origin = 3_000u64;
6108 assert_eq!(3_000u64.saturating_sub(origin), 0);
6109 assert_eq!(3_040u64.saturating_sub(origin), 40);
6110 assert_eq!(wmv_rebase_duration_ms(63_000, origin), Some(60_000));
6111 }
6112
6113 #[test]
6114 fn audio_alignment_uses_video_origin() {
6115 assert_eq!(wmv_audio_alignment_frames(3_000, 3_000, 48_000), (0, 0));
6116 assert_eq!(wmv_audio_alignment_frames(3_100, 3_000, 48_000), (4_800, 0));
6117 assert_eq!(wmv_audio_alignment_frames(2_900, 3_000, 48_000), (0, 4_800));
6118 }
6119
6120 #[test]
6121 fn recognizes_siglus_wmv_extension() {
6122 assert!(is_wmv_extension("wmv"));
6123 assert!(!is_wmv_extension("asf"));
6124 assert!(!is_wmv_extension("mpg"));
6125 }
6126
6127 #[test]
6128 fn frame_selection_follows_asf_pts() {
6129 let frames = VecDeque::from([frame(0, 3_000), frame(1, 3_040), frame(2, 3_120)]);
6130 let selected = select_wmv_stream_frame(&frames, 3_080).expect("selected frame");
6131 assert_eq!(selected.frame_idx, 1);
6132 }
6133
6134 #[test]
6135 fn frame_selection_never_presents_future_pts() {
6136 let frames = VecDeque::from([frame(7, 3_040), frame(8, 3_080)]);
6137 assert!(select_wmv_stream_frame(&frames, 3_020).is_none());
6138 assert_eq!(
6139 select_wmv_stream_frame(&frames, 3_040).map(|frame| frame.frame_idx),
6140 Some(7)
6141 );
6142 }
6143
6144 #[test]
6145 fn b_picture_decode_order_does_not_choose_clock_origin() {
6146 let mut frames = VecDeque::new();
6147 for decoded in [
6148 frame(0, 3_000),
6149 frame(1, 3_120),
6150 frame(2, 3_040),
6151 frame(3, 3_080),
6152 ] {
6153 let pos = frames
6154 .iter()
6155 .position(|queued: &WmvDecodedFrame| queued.source_pts_ms > decoded.source_pts_ms)
6156 .unwrap_or(frames.len());
6157 frames.insert(pos, decoded);
6158 }
6159 assert_eq!(
6160 frames.iter().map(|f| f.source_pts_ms).collect::<Vec<_>>(),
6161 vec![3_000, 3_040, 3_080, 3_120]
6162 );
6163 let origin = frames
6164 .front()
6165 .expect("first presentation frame")
6166 .source_pts_ms;
6167 assert_eq!(origin, 3_000);
6168 assert_eq!(
6169 select_wmv_stream_frame(&frames, origin + 80).map(|frame| frame.frame_idx),
6170 Some(3)
6171 );
6172 }
6173
6174 #[test]
6175 fn frame_duration_uses_median_positive_pts_delta() {
6176 assert_eq!(wmv_frame_duration_ms(&[0, 40, 80, 200]), Some(40));
6177 }
6178
6179 #[test]
6180 fn yuv_black_converts_to_opaque_black() {
6181 let yuv = wmv_decoder::YuvFrame {
6182 width: 2,
6183 height: 2,
6184 y: vec![16; 4],
6185 cb: vec![128],
6186 cr: vec![128],
6187 };
6188 let rgba = wmv_yuv_frame_to_rgba(&yuv);
6189 assert_eq!(rgba.rgba, [0, 0, 0, 255].repeat(4));
6190 }
6191}