1use std::collections::{HashMap, VecDeque};
2use std::fs;
3use std::io::{Cursor, Read};
4use std::path::{Path, PathBuf};
5use std::sync::{
6 mpsc::{self, Receiver, TryRecvError},
7 Arc,
8};
9use std::sync::atomic::{AtomicUsize, Ordering};
10use std::thread;
11use crate::platform_time::{Duration, Instant};
12
13use anyhow::{anyhow, bail, Context, Result};
14use kira::sound::static_sound::{StaticSoundData, StaticSoundHandle};
15
16use crate::assets::RgbaImage;
17use crate::audio::{AudioHub, TrackKind};
18
19const MPEG2_HEADER_PROBE_BYTES: usize = 256 * 1024;
20const MPEG2_STREAM_CHUNK_BYTES: usize = 256 * 1024;
21const MPEG2_STREAM_CHANNEL_CAPACITY: usize = 4;
22const MPEG2_STREAM_MAX_DRAIN_EVENTS: usize = 8;
23const MPEG2_STREAM_FRAME_KEEP: usize = 6;
24const MPEG2_STREAM_DECODE_LEAD_FRAMES: usize = 3;
25const OMV_STREAM_CHANNEL_CAPACITY: usize = 12;
26const OMV_STREAM_MAX_DRAIN_EVENTS: usize = 16;
27const OMV_STREAM_FRAME_KEEP: usize = 16;
28const OMV_STREAM_DECODE_LEAD_FRAMES: usize = 4;
29
30#[derive(Debug, Clone)]
31pub struct MovieInfo {
32 pub path: PathBuf,
33 pub width: Option<u32>,
34 pub height: Option<u32>,
35 pub fps: Option<f32>,
36 pub decoded_frames: Option<usize>,
37 pub audio_duration_ms: Option<u64>,
38}
39
40impl MovieInfo {
41 pub fn duration_ms(&self) -> Option<u64> {
42 if let Some(ms) = self.audio_duration_ms {
43 return Some(ms);
44 }
45 let fps = self.fps?;
46 let frames = self.decoded_frames?;
47 if fps <= 0.0 || frames == 0 {
48 return None;
49 }
50 Some(((frames as f64) * 1000.0 / (fps as f64)).round() as u64)
51 }
52}
53
54#[derive(Debug, Clone)]
55pub struct MovieStreamFrame {
56 pub frame: Arc<RgbaImage>,
57 pub frame_idx: usize,
58 pub fps: Option<f32>,
59 pub total_ms: Option<u64>,
60 pub audio: Option<MovieAudio>,
61 pub audio_ready: bool,
62 pub decoded_now: bool,
63 pub clamped_timer_ms: Option<u64>,
64}
65
66enum Mpeg2StreamEvent {
67 Info {
68 width: Option<u32>,
69 height: Option<u32>,
70 fps: Option<f32>,
71 },
72 Video {
73 frame_idx: usize,
74 frame: Arc<RgbaImage>,
75 },
76 Done,
77}
78
79struct Mpeg2StreamState {
80 rx: Receiver<Result<Mpeg2StreamEvent, String>>,
81 frames: VecDeque<(usize, Arc<RgbaImage>)>,
82 width: Option<u32>,
83 height: Option<u32>,
84 fps: Option<f32>,
85 decoded_frames: usize,
86 done: bool,
87 audio: Option<MovieAudio>,
88 decoded_any_this_poll: bool,
89 request_frames: Arc<AtomicUsize>,
90}
91
92impl Drop for Mpeg2StreamState {
93 fn drop(&mut self) {
94 self.request_frames.store(usize::MAX, Ordering::Release);
95 }
96}
97
98enum OmvStreamEvent {
99 Info {
100 width: u32,
101 height: u32,
102 fps: Option<f32>,
103 frame_time_ms: Option<f64>,
104 total_frames_hint: Option<usize>,
105 },
106 Video {
107 frame_idx: usize,
108 frame: Arc<RgbaImage>,
109 },
110 Done,
111}
112
113struct OmvStreamState {
114 rx: Receiver<Result<OmvStreamEvent, String>>,
115 frames: VecDeque<(usize, Arc<RgbaImage>)>,
116 width: Option<u32>,
117 height: Option<u32>,
118 fps: Option<f32>,
119 frame_time_ms: Option<f64>,
120 total_frames_hint: Option<usize>,
121 decoded_frames: usize,
122 done: bool,
123 request_frames: Arc<AtomicUsize>,
124}
125
126impl Drop for OmvStreamState {
127 fn drop(&mut self) {
128 self.request_frames.store(usize::MAX, Ordering::Release);
129 }
130}
131
132pub struct MovieManager {
139 project_dir: PathBuf,
140 current_append_dir: String,
141 current: Option<MovieInfo>,
142 cache: HashMap<PathBuf, MovieAsset>,
143 preview_cache: HashMap<PathBuf, Arc<RgbaImage>>,
144 decode_tasks: HashMap<PathBuf, Receiver<Result<MovieAsset, String>>>,
145 mpeg2_audio_cache: HashMap<PathBuf, Option<MovieAudio>>,
146 mpeg2_streams: HashMap<PathBuf, Mpeg2StreamState>,
147 omv_streams: HashMap<PathBuf, OmvStreamState>,
148 playbacks: HashMap<u64, MoviePlayback>,
149 next_playback_id: u64,
150}
151
152impl std::fmt::Debug for MovieManager {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.debug_struct("MovieManager")
155 .field("project_dir", &self.project_dir)
156 .field("current_append_dir", &self.current_append_dir)
157 .field("current", &self.current)
158 .field("cache_len", &self.cache.len())
159 .field("preview_cache_len", &self.preview_cache.len())
160 .field("decode_tasks_len", &self.decode_tasks.len())
161 .field("mpeg2_audio_cache_len", &self.mpeg2_audio_cache.len())
162 .field("mpeg2_streams_len", &self.mpeg2_streams.len())
163 .field("omv_streams_len", &self.omv_streams.len())
164 .field("playbacks_len", &self.playbacks.len())
165 .finish()
166 }
167}
168
169impl MovieManager {
170 pub fn new(project_dir: PathBuf) -> Self {
171 Self {
172 project_dir,
173 current_append_dir: String::new(),
174 current: None,
175 cache: HashMap::new(),
176 preview_cache: HashMap::new(),
177 decode_tasks: HashMap::new(),
178 mpeg2_audio_cache: HashMap::new(),
179 mpeg2_streams: HashMap::new(),
180 omv_streams: HashMap::new(),
181 playbacks: HashMap::new(),
182 next_playback_id: 1,
183 }
184 }
185
186 pub fn current(&self) -> Option<&MovieInfo> {
187 self.current.as_ref()
188 }
189
190 pub fn set_current_append_dir(&mut self, append_dir: impl Into<String>) {
191 self.current_append_dir = append_dir.into();
192 }
193
194 pub fn stop(&mut self) {
195 self.current = None;
196 self.mpeg2_streams.clear();
197 self.omv_streams.clear();
198 }
199
200 pub fn prepare(&mut self, file_name: &str) -> Result<MovieInfo> {
201 self.play(file_name, false, false)
202 }
203
204 pub fn prepare_omv(&mut self, file_name: &str) -> Result<MovieInfo> {
205 let path = crate::resource::find_omv_path_with_append_dir(
206 &self.project_dir,
207 &self.current_append_dir,
208 file_name,
209 )?;
210 let header = read_omv_header_for_path(&path)
211 .with_context(|| format!("open OMV: {}", path.display()))?;
212 let w = header.display_width;
213 let h = header.display_height;
214 let fps = if header.frame_time_us != 0 {
215 Some(1_000_000.0 / (header.frame_time_us as f32))
216 } else {
217 None
218 };
219 let info = MovieInfo {
220 path,
221 width: (w > 0).then_some(w),
222 height: (h > 0).then_some(h),
223 fps,
224 decoded_frames: (header.packet_count_hint > 0)
225 .then_some(header.packet_count_hint as usize),
226 audio_duration_ms: None,
227 };
228 self.current = Some(info.clone());
229 Ok(info)
230 }
231
232 pub fn play(&mut self, file_name: &str, _wait: bool, _key_skip: bool) -> Result<MovieInfo> {
233 let path = resolve_mov_path(&self.project_dir, &self.current_append_dir, file_name)?;
234 let ext = path
235 .extension()
236 .and_then(|s| s.to_str())
237 .unwrap_or("")
238 .to_ascii_lowercase();
239
240 let info = if ext == "omv" {
241 let header = read_omv_header_for_path(&path)
242 .with_context(|| format!("open OMV: {}", path.display()))?;
243 let w = header.display_width;
244 let h = header.display_height;
245 let fps = if header.frame_time_us != 0 {
246 Some(1_000_000.0 / (header.frame_time_us as f32))
247 } else {
248 None
249 };
250 MovieInfo {
251 path,
252 width: (w > 0).then_some(w),
253 height: (h > 0).then_some(h),
254 fps,
255 decoded_frames: (header.packet_count_hint > 0)
256 .then_some(header.packet_count_hint as usize),
257 audio_duration_ms: None,
258 }
259 } else {
260 let prefix = read_file_prefix(&path, MPEG2_HEADER_PROBE_BYTES)
261 .with_context(|| format!("read movie header: {}", path.display()))?;
262
263 let mut width = None;
264 let mut height = None;
265 let mut fps = None;
266
267 if let Some(h) = siglus_assets::mpeg2::find_sequence_header(&prefix) {
268 width = Some(h.width as u32);
269 height = Some(h.height as u32);
270 fps = siglus_assets::mpeg2::fps_from_frame_rate_code(h.frame_rate_code);
271 }
272
273 let decoded_frames = decode_frames_if_enabled(&path)?;
274
275 MovieInfo {
276 path,
277 width,
278 height,
279 fps,
280 decoded_frames,
281 audio_duration_ms: None,
282 }
283 };
284
285 self.current = Some(info.clone());
286 Ok(info)
287 }
288
289 pub fn ensure_asset(&mut self, file_name: &str) -> Result<(&MovieAsset, bool)> {
291 let path = resolve_mov_path(&self.project_dir, &self.current_append_dir, file_name)?;
292 self.ensure_asset_for_path(path)
293 }
294
295 pub fn ensure_omv_asset(&mut self, file_name: &str) -> Result<(&MovieAsset, bool)> {
296 let path = crate::resource::find_omv_path_with_append_dir(
297 &self.project_dir,
298 &self.current_append_dir,
299 file_name,
300 )?;
301 self.ensure_asset_for_path(path)
302 }
303
304 fn ensure_asset_for_path(&mut self, path: PathBuf) -> Result<(&MovieAsset, bool)> {
305 let existed = self.cache.contains_key(&path);
306 if !existed {
307 let asset = decode_asset_for_path(&path)?;
308 self.cache.insert(path.clone(), asset);
309 }
310 let asset = self.cache.get(&path).expect("asset cached");
311 Ok((asset, !existed))
312 }
313
314 pub fn poll_asset(&mut self, file_name: &str) -> Result<Option<(&MovieAsset, bool)>> {
315 let path = resolve_mov_path(&self.project_dir, &self.current_append_dir, file_name)?;
316 self.poll_asset_for_path(path)
317 }
318
319 pub fn poll_omv_asset(&mut self, file_name: &str) -> Result<Option<(&MovieAsset, bool)>> {
320 let path = crate::resource::find_omv_path_with_append_dir(
321 &self.project_dir,
322 &self.current_append_dir,
323 file_name,
324 )?;
325 self.poll_asset_for_path(path)
326 }
327
328 fn poll_asset_for_path(&mut self, path: PathBuf) -> Result<Option<(&MovieAsset, bool)>> {
329 if self.cache.contains_key(&path) {
330 let asset = self.cache.get(&path).expect("asset cached");
331 return Ok(Some((asset, false)));
332 }
333
334 let mut completed = None;
335 let mut failed = None;
336 if let Some(rx) = self.decode_tasks.get(&path) {
337 match rx.try_recv() {
338 Ok(Ok(asset)) => completed = Some(asset),
339 Ok(Err(err)) => failed = Some(err),
340 Err(TryRecvError::Empty) => {}
341 Err(TryRecvError::Disconnected) => {
342 failed = Some(format!(
343 "movie decode worker disconnected: {}",
344 path.display()
345 ));
346 }
347 }
348 } else {
349 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
350 {
351 let asset = decode_asset_for_path(&path)?;
352 self.cache.insert(path.clone(), asset);
353 let asset = self.cache.get(&path).expect("asset cached");
354 return Ok(Some((asset, true)));
355 }
356 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
357 {
358 let (tx, rx) = mpsc::channel();
359 let worker_path = path.clone();
360 thread::spawn(move || {
361 let result = decode_asset_for_path(&worker_path).map_err(|e| format!("{:#}", e));
362 let _ = tx.send(result);
363 });
364 self.decode_tasks.insert(path.clone(), rx);
365 }
366 }
367
368 if let Some(err) = failed {
369 self.decode_tasks.remove(&path);
370 return Err(anyhow!(err));
371 }
372 if let Some(asset) = completed {
373 self.decode_tasks.remove(&path);
374 self.cache.insert(path.clone(), asset);
375 let asset = self.cache.get(&path).expect("asset cached");
376 return Ok(Some((asset, true)));
377 }
378
379 Ok(None)
380 }
381
382 pub fn poll_global_movie_frame(
383 &mut self,
384 file_name: &str,
385 timer_ms: u64,
386 ) -> Result<Option<MovieStreamFrame>> {
387 self.poll_global_movie_frame_with_loop(file_name, timer_ms, false)
388 }
389
390 pub fn poll_global_movie_frame_with_loop(
391 &mut self,
392 file_name: &str,
393 timer_ms: u64,
394 loop_flag: bool,
395 ) -> Result<Option<MovieStreamFrame>> {
396 let path = resolve_mov_path(&self.project_dir, &self.current_append_dir, file_name)?;
397 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
398 {
399 return self.poll_cached_movie_frame_for_path_with_loop(path, timer_ms, loop_flag);
400 }
401 let ext = path
402 .extension()
403 .and_then(|s| s.to_str())
404 .unwrap_or("")
405 .to_ascii_lowercase();
406 if ext == "omv" {
407 return self.poll_omv_stream_frame_for_path(path, timer_ms, loop_flag);
408 }
409 self.poll_mpeg2_stream_frame_for_path(path, timer_ms)
410 }
411
412 fn poll_cached_movie_frame_for_path(
413 &mut self,
414 path: PathBuf,
415 timer_ms: u64,
416 ) -> Result<Option<MovieStreamFrame>> {
417 self.poll_cached_movie_frame_for_path_with_loop(path, timer_ms, false)
418 }
419
420 fn poll_cached_movie_frame_for_path_with_loop(
421 &mut self,
422 path: PathBuf,
423 timer_ms: u64,
424 loop_flag: bool,
425 ) -> Result<Option<MovieStreamFrame>> {
426 let (asset, decoded_now) = match self.poll_asset_for_path(path)? {
427 Some(v) => v,
428 None => return Ok(None),
429 };
430 if asset.frames.is_empty() {
431 return Ok(None);
432 }
433
434 let fps = asset.info.fps.unwrap_or_else(|| {
435 asset
436 .info
437 .duration_ms()
438 .filter(|ms| *ms > 0)
439 .map(|ms| (asset.frames.len() as f32) * 1000.0 / (ms as f32))
440 .unwrap_or(0.0)
441 });
442 let effective_timer_ms = if loop_flag {
443 asset
444 .info
445 .duration_ms()
446 .filter(|ms| *ms > 0)
447 .map(|ms| timer_ms % ms)
448 .unwrap_or(timer_ms)
449 } else {
450 timer_ms
451 };
452 let mut idx = frame_index_for_timer(effective_timer_ms, fps, asset.frames.len());
453 if idx >= asset.frames.len() {
454 idx = asset.frames.len() - 1;
455 }
456
457 Ok(Some(MovieStreamFrame {
458 frame: asset.frames[idx].clone(),
459 frame_idx: idx,
460 fps: (fps > 0.0).then_some(fps),
461 total_ms: asset.info.duration_ms(),
462 audio: asset.audio.clone(),
463 audio_ready: true,
464 decoded_now,
465 clamped_timer_ms: None,
466 }))
467 }
468
469 fn poll_mpeg2_stream_frame_for_path(
470 &mut self,
471 path: PathBuf,
472 timer_ms: u64,
473 ) -> Result<Option<MovieStreamFrame>> {
474 let audio = self.ensure_mpeg2_audio_for_path(path.as_path());
475 if !self.mpeg2_streams.contains_key(&path) {
476 let state = spawn_mpeg2_stream_state(path.clone(), audio.clone())?;
477 self.mpeg2_streams.insert(path.clone(), state);
478 }
479
480 let desired_before_drain = self.mpeg2_streams.get(&path).and_then(|state| {
481 state
482 .fps
483 .filter(|f| *f > 0.0)
484 .map(|fps| ((timer_ms as f64) * (fps as f64) / 1000.0).floor() as usize)
485 });
486 let restart_stream = desired_before_drain
487 .and_then(|desired| {
488 self.mpeg2_streams.get(&path).map(|state| {
489 let front_after_target = state
490 .frames
491 .front()
492 .map(|(idx, _)| *idx > desired)
493 .unwrap_or(false);
494 let decoder_already_past_target = state.frames.is_empty()
495 && state.decoded_frames > desired.saturating_add(MPEG2_STREAM_DECODE_LEAD_FRAMES)
496 && !state.done;
497 front_after_target || decoder_already_past_target
498 })
499 })
500 .unwrap_or(false);
501 if restart_stream {
502 self.mpeg2_streams.remove(&path);
503 let state = spawn_mpeg2_stream_state(path.clone(), audio)?;
504 self.mpeg2_streams.insert(path.clone(), state);
505 }
506
507 let state = self
508 .mpeg2_streams
509 .get_mut(&path)
510 .expect("mpeg2 stream state exists");
511 let request_until = desired_before_drain
512 .unwrap_or(0)
513 .saturating_add(MPEG2_STREAM_DECODE_LEAD_FRAMES);
514 state.request_frames.store(request_until, Ordering::Release);
515 drain_mpeg2_stream_state(path.as_path(), state, desired_before_drain, timer_ms)?;
516
517 if state.frames.is_empty() {
518 return Ok(None);
519 }
520
521 let latest_idx = state.decoded_frames.saturating_sub(1);
522 let desired_idx = desired_before_drain.unwrap_or(latest_idx);
523 let chosen_idx = desired_idx.min(latest_idx);
524
525 let Some((actual_frame_idx, frame)) = select_stream_frame(&state.frames, chosen_idx) else {
526 return Ok(None);
527 };
528
529 let video_total_ms = if state.done && state.decoded_frames > 0 {
530 state
531 .fps
532 .filter(|f| *f > 0.0)
533 .map(|fps| ((state.decoded_frames as f64) * 1000.0 / (fps as f64)).round() as u64)
534 } else {
535 None
536 };
537 let audio_total_ms = state.audio.as_ref().map(|a| a.end_ms());
538 let total_ms = match (audio_total_ms, video_total_ms) {
539 (Some(a), Some(v)) => Some(a.max(v)),
540 (Some(a), None) => Some(a),
541 (None, Some(v)) => Some(v),
542 (None, None) => None,
543 };
544
545 let audio = state
546 .audio
547 .as_ref()
548 .filter(|track| timer_ms < track.end_ms())
549 .cloned();
550 state.decoded_any_this_poll = false;
551
552 Ok(Some(MovieStreamFrame {
553 frame,
554 frame_idx: actual_frame_idx,
555 fps: state.fps,
556 total_ms,
557 audio,
558 audio_ready: true,
559 decoded_now: false,
560 clamped_timer_ms: None,
561 }))
562 }
563
564 fn ensure_mpeg2_audio_for_path(&mut self, path: &Path) -> Option<MovieAudio> {
565 if let Some(audio) = self.mpeg2_audio_cache.get(path) {
566 return audio.clone();
567 }
568 let audio = match decode_mpeg2_audio_for_path(path) {
569 Ok(audio) => audio,
570 Err(err) => {
571 eprintln!(
572 "[SG_MOV] mpeg2 audio predecode failed path={} err={:#}",
573 path.display(),
574 err
575 );
576 None
577 }
578 };
579 self.mpeg2_audio_cache
580 .insert(path.to_path_buf(), audio.clone());
581 audio
582 }
583
584 fn poll_omv_stream_frame_for_path(
585 &mut self,
586 path: PathBuf,
587 timer_ms: u64,
588 loop_flag: bool,
589 ) -> Result<Option<MovieStreamFrame>> {
590 if !self.omv_streams.contains_key(&path) {
591 let state = spawn_omv_stream_state(path.clone())?;
592 self.omv_streams.insert(path.clone(), state);
593 }
594
595 let desired_before_drain = self.omv_streams.get(&path).and_then(|state| {
596 if let Some(ms) = state.frame_time_ms.filter(|v| *v > 0.0) {
597 Some(((timer_ms as f64) / ms).floor() as usize)
598 } else {
599 state
600 .fps
601 .filter(|f| *f > 0.0)
602 .map(|fps| ((timer_ms as f64) * (fps as f64) / 1000.0).floor() as usize)
603 }
604 });
605 let restart_stream = desired_before_drain
606 .and_then(|desired| {
607 self.omv_streams.get(&path).map(|state| {
608 let cached_target = state.frames.iter().any(|(idx, _)| *idx == desired);
609 let front_after_target = state
610 .frames
611 .front()
612 .map(|(idx, _)| *idx > desired)
613 .unwrap_or(false);
614 let decoder_already_past_target = state.frames.is_empty()
615 && state.decoded_frames > desired.saturating_add(OMV_STREAM_DECODE_LEAD_FRAMES)
616 && !state.done;
617 !cached_target && (front_after_target || decoder_already_past_target)
618 })
619 })
620 .unwrap_or(false);
621 if restart_stream {
622 self.omv_streams.remove(&path);
623 let state = spawn_omv_stream_state(path.clone())?;
624 self.omv_streams.insert(path.clone(), state);
625 }
626
627 let state = self
628 .omv_streams
629 .get_mut(&path)
630 .expect("omv stream state exists");
631 let mut request_until = desired_before_drain
632 .unwrap_or(0)
633 .saturating_add(OMV_STREAM_DECODE_LEAD_FRAMES);
634 if loop_flag {
635 if let Some(total_frames) = state.total_frames_hint.filter(|v| *v > 0) {
636 request_until = request_until.max(total_frames.saturating_sub(1));
637 }
638 }
639 state.request_frames.store(request_until, Ordering::Release);
640 drain_omv_stream_state(path.as_path(), state, desired_before_drain, loop_flag)?;
641 if loop_flag {
642 if let Some(total_frames) = state.total_frames_hint.filter(|v| *v > 0) {
643 state
644 .request_frames
645 .store(total_frames.saturating_sub(1), Ordering::Release);
646 }
647 }
648
649 if state.frames.is_empty() {
650 return Ok(None);
651 }
652
653 let latest_idx = state.decoded_frames.saturating_sub(1);
654 let desired_idx = desired_before_drain.unwrap_or(latest_idx);
655 let chosen_idx = desired_idx.min(latest_idx);
656 let Some((actual_frame_idx, frame)) = select_stream_frame(&state.frames, chosen_idx) else {
657 return Ok(None);
658 };
659
660 let video_total_ms = if state.done && state.decoded_frames > 0 {
661 state
662 .frame_time_ms
663 .map(|ms| ((state.decoded_frames as f64) * ms).round() as u64)
664 .or_else(|| {
665 state
666 .fps
667 .filter(|f| *f > 0.0)
668 .map(|fps| ((state.decoded_frames as f64) * 1000.0 / (fps as f64)).round() as u64)
669 })
670 } else {
671 state.total_frames_hint.and_then(|frames| {
672 state
673 .frame_time_ms
674 .map(|ms| ((frames as f64) * ms).round() as u64)
675 .or_else(|| {
676 state
677 .fps
678 .filter(|f| *f > 0.0)
679 .map(|fps| ((frames as f64) * 1000.0 / (fps as f64)).round() as u64)
680 })
681 })
682 };
683
684 Ok(Some(MovieStreamFrame {
685 frame,
686 frame_idx: actual_frame_idx,
687 fps: state.fps,
688 total_ms: video_total_ms,
689 audio: None,
690 audio_ready: true,
691 decoded_now: false,
692 clamped_timer_ms: None,
693 }))
694 }
695
696 pub fn ensure_preview_frame(&mut self, file_name: &str) -> Result<Arc<RgbaImage>> {
697 let path = resolve_mov_path(&self.project_dir, &self.current_append_dir, file_name)?;
698 self.ensure_preview_frame_for_path(path)
699 }
700
701 pub fn ensure_omv_preview_frame(&mut self, file_name: &str) -> Result<Arc<RgbaImage>> {
702 let path = crate::resource::find_omv_path_with_append_dir(
703 &self.project_dir,
704 &self.current_append_dir,
705 file_name,
706 )?;
707 self.ensure_preview_frame_for_path(path)
708 }
709
710 fn ensure_preview_frame_for_path(&mut self, path: PathBuf) -> Result<Arc<RgbaImage>> {
711 if let Some(frame) = self.preview_cache.get(&path) {
712 return Ok(frame.clone());
713 }
714 let ext = path
715 .extension()
716 .and_then(|s| s.to_str())
717 .unwrap_or("")
718 .to_ascii_lowercase();
719 let frame = if ext == "omv" {
720 decode_omv_preview_frame(&path)?
721 } else {
722 decode_mpeg2_preview_frame(&path)?
723 };
724 self.preview_cache.insert(path, frame.clone());
725 Ok(frame)
726 }
727 pub fn start_audio(
728 &mut self,
729 audio: &mut AudioHub,
730 track: &MovieAudio,
731 offset_ms: u64,
732 ) -> Result<u64> {
733 let local_offset_ms = offset_ms.saturating_sub(track.start_ms);
734 let remaining_ms = track
735 .duration_ms
736 .map(|d| d.saturating_sub(local_offset_ms.min(d)));
737 let wav = encode_wav_i16_interleaved_offset(track, local_offset_ms);
738 let data = StaticSoundData::from_cursor(Cursor::new(wav))
739 .context("kira: decode movie WAV bytes")?;
740 let handle = audio.play_static(TrackKind::Mov, data)?;
741 let id = self.next_playback_id;
742 self.next_playback_id = self.next_playback_id.saturating_add(1).max(1);
743 self.playbacks.insert(
744 id,
745 MoviePlayback {
746 handle,
747 started_at: Instant::now(),
748 duration_ms: remaining_ms,
749 },
750 );
751 Ok(id)
752 }
753
754 pub fn pause_audio(&mut self, id: u64) {
755 let Some(p) = self.playbacks.get_mut(&id) else {
756 return;
757 };
758 let _ = p.handle.pause(kira::tween::Tween::default());
759 }
760
761 pub fn resume_audio(&mut self, id: u64) {
762 let Some(p) = self.playbacks.get_mut(&id) else {
763 return;
764 };
765 let _ = p.handle.resume(kira::tween::Tween::default());
766 }
767
768 pub fn stop_audio(&mut self, id: u64) {
769 if let Some(mut p) = self.playbacks.remove(&id) {
770 let _ = p.handle.stop(kira::tween::Tween::default());
771 }
772 }
773
774 pub fn audio_playback_finished(&mut self, id: u64) -> bool {
775 let Some(p) = self.playbacks.get(&id) else {
776 return true;
777 };
778 let Some(duration_ms) = p.duration_ms else {
779 return false;
780 };
781 if p.started_at.elapsed() >= Duration::from_millis(duration_ms) {
782 if let Some(mut p) = self.playbacks.remove(&id) {
783 let _ = p.handle.stop(kira::tween::Tween::default());
784 }
785 true
786 } else {
787 false
788 }
789 }
790}
791
792fn resolve_mov_path(
793 project_dir: &Path,
794 current_append_dir: &str,
795 file_name: &str,
796) -> Result<PathBuf> {
797 let (path, _ty) =
798 crate::resource::find_mov_path_with_append_dir(project_dir, current_append_dir, file_name)?;
799 Ok(path)
800}
801
802fn decode_frames_if_enabled(_path: &Path) -> Result<Option<usize>> {
803 Ok(None)
804}
805
806fn read_file_prefix(path: &Path, max_len: usize) -> Result<Vec<u8>> {
807 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
808 {
809 let mut out = read_movie_bytes(path)?;
810 out.truncate(max_len.min(out.len()));
811 return Ok(out);
812 }
813 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
814 {
815 let mut file = fs::File::open(path).with_context(|| format!("open file: {}", path.display()))?;
816 let mut out = vec![0u8; max_len.max(1)];
817 let n = file
818 .read(&mut out)
819 .with_context(|| format!("read file prefix: {}", path.display()))?;
820 out.truncate(n);
821 Ok(out)
822 }
823}
824
825fn frame_index_for_timer(timer_ms: u64, fps: f32, frame_count: usize) -> usize {
826 if frame_count == 0 {
827 return 0;
828 }
829 if fps <= 0.0 {
830 return 0;
831 }
832 ((timer_ms as f64) * (fps as f64) / 1000.0).floor() as usize
833}
834
835fn spawn_mpeg2_stream_state(path: PathBuf, audio: Option<MovieAudio>) -> Result<Mpeg2StreamState> {
836 let prefix = read_file_prefix(&path, MPEG2_HEADER_PROBE_BYTES)?;
837 let mut width = None;
838 let mut height = None;
839 let mut fps = None;
840 if let Some(h) = siglus_assets::mpeg2::find_sequence_header(&prefix) {
841 width = Some(h.width as u32);
842 height = Some(h.height as u32);
843 fps = siglus_assets::mpeg2::fps_from_frame_rate_code(h.frame_rate_code);
844 }
845
846 let (tx, rx) = mpsc::sync_channel(MPEG2_STREAM_CHANNEL_CAPACITY);
847 let request_frames = Arc::new(AtomicUsize::new(MPEG2_STREAM_DECODE_LEAD_FRAMES));
848 let worker_request_frames = request_frames.clone();
849 let video_path = path.clone();
850 thread::spawn(move || {
851 let result = stream_mpeg2_video_worker(video_path.as_path(), tx.clone(), worker_request_frames);
852 if let Err(err) = result {
853 let _ = tx.send(Err(format!("{:#}", err)));
854 }
855 });
856
857 Ok(Mpeg2StreamState {
858 rx,
859 frames: VecDeque::new(),
860 width,
861 height,
862 fps,
863 decoded_frames: 0,
864 done: false,
865 audio,
866 decoded_any_this_poll: false,
867 request_frames,
868 })
869}
870
871fn wait_for_mpeg2_frame_request(request_frames: &Arc<AtomicUsize>, frame_idx: usize) {
872 while frame_idx > request_frames.load(Ordering::Acquire) {
873 if request_frames.load(Ordering::Acquire) == usize::MAX {
874 return;
875 }
876 thread::sleep(Duration::from_millis(1));
877 }
878}
879
880fn stream_mpeg2_video_worker(
881 path: &Path,
882 tx: mpsc::SyncSender<Result<Mpeg2StreamEvent, String>>,
883 request_frames: Arc<AtomicUsize>,
884) -> Result<()> {
885 let prefix = read_file_prefix(path, MPEG2_HEADER_PROBE_BYTES)?;
886 let mut width = None;
887 let mut height = None;
888 let mut fps = None;
889 if let Some(h) = siglus_assets::mpeg2::find_sequence_header(&prefix) {
890 width = Some(h.width as u32);
891 height = Some(h.height as u32);
892 fps = siglus_assets::mpeg2::fps_from_frame_rate_code(h.frame_rate_code);
893 }
894 if tx
895 .send(Ok(Mpeg2StreamEvent::Info { width, height, fps }))
896 .is_err()
897 {
898 return Ok(());
899 }
900
901 let mut file = fs::File::open(path).with_context(|| format!("open movie file: {}", path.display()))?;
902 let mut pipeline = na_mpeg2_decoder::MpegVideoPipeline::new();
903 let mut buf = vec![0u8; MPEG2_STREAM_CHUNK_BYTES];
904 let mut frame_idx = 0usize;
905 let mut send_failed = false;
906
907 loop {
908 let n = file
909 .read(&mut buf)
910 .with_context(|| format!("read movie stream: {}", path.display()))?;
911 if n == 0 {
912 break;
913 }
914 pipeline
915 .push_with(&buf[..n], None, |f| {
916 if send_failed {
917 return;
918 }
919 wait_for_mpeg2_frame_request(&request_frames, frame_idx);
920 if request_frames.load(Ordering::Acquire) == usize::MAX {
921 send_failed = true;
922 return;
923 }
924 let w = f.width as u32;
925 let h = f.height as u32;
926 let mut rgba = vec![0u8; (w as usize).saturating_mul(h as usize).saturating_mul(4)];
927 na_mpeg2_decoder::frame_to_rgba_bt601_limited(&f, &mut rgba);
928 let frame = Arc::new(RgbaImage {
929 width: w,
930 height: h,
931 center_x: 0,
932 center_y: 0,
933 rgba,
934 });
935 let ev = Mpeg2StreamEvent::Video { frame_idx, frame };
936 frame_idx = frame_idx.saturating_add(1);
937 if tx.send(Ok(ev)).is_err() {
938 send_failed = true;
939 }
940 })
941 .context("mpeg2 stream video decode")?;
942 if send_failed {
943 return Ok(());
944 }
945 }
946
947 pipeline.flush_with(|f| {
948 if send_failed {
949 return;
950 }
951 wait_for_mpeg2_frame_request(&request_frames, frame_idx);
952 if request_frames.load(Ordering::Acquire) == usize::MAX {
953 send_failed = true;
954 return;
955 }
956 let w = f.width as u32;
957 let h = f.height as u32;
958 let mut rgba = vec![0u8; (w as usize).saturating_mul(h as usize).saturating_mul(4)];
959 na_mpeg2_decoder::frame_to_rgba_bt601_limited(&f, &mut rgba);
960 let frame = Arc::new(RgbaImage {
961 width: w,
962 height: h,
963 center_x: 0,
964 center_y: 0,
965 rgba,
966 });
967 let ev = Mpeg2StreamEvent::Video { frame_idx, frame };
968 frame_idx = frame_idx.saturating_add(1);
969 if tx.send(Ok(ev)).is_err() {
970 send_failed = true;
971 }
972 })?;
973
974 if !send_failed {
975 let _ = tx.send(Ok(Mpeg2StreamEvent::Done));
976 }
977 Ok(())
978}
979
980fn drain_mpeg2_stream_state(
981 path: &Path,
982 state: &mut Mpeg2StreamState,
983 target_frame_idx: Option<usize>,
984 _target_timer_ms: u64,
985) -> Result<()> {
986 state.decoded_any_this_poll = false;
987
988 let decode_until = target_frame_idx
989 .map(|idx| idx.saturating_add(MPEG2_STREAM_DECODE_LEAD_FRAMES));
990
991 for _ in 0..MPEG2_STREAM_MAX_DRAIN_EVENTS {
992 if let Some(limit) = decode_until {
993 if state.decoded_frames > limit && !state.frames.is_empty() {
994 break;
995 }
996 }
997 match state.rx.try_recv() {
998 Ok(Ok(Mpeg2StreamEvent::Info { width, height, fps })) => {
999 state.width = width.or(state.width);
1000 state.height = height.or(state.height);
1001 state.fps = fps.or(state.fps);
1002 }
1003 Ok(Ok(Mpeg2StreamEvent::Video { frame_idx, frame })) => {
1004 state.decoded_frames = state.decoded_frames.max(frame_idx.saturating_add(1));
1005 state.frames.push_back((frame_idx, frame));
1006 state.decoded_any_this_poll = true;
1007 }
1008 Ok(Ok(Mpeg2StreamEvent::Done)) => {
1009 state.done = true;
1010 break;
1011 }
1012 Ok(Err(err)) => {
1013 return Err(anyhow!("mpeg2 stream decode failed for {}: {}", path.display(), err));
1014 }
1015 Err(TryRecvError::Empty) => break,
1016 Err(TryRecvError::Disconnected) => {
1017 state.done = true;
1018 break;
1019 }
1020 }
1021 }
1022
1023 if let Some(target) = target_frame_idx {
1024 let keep_from = target.saturating_sub(2);
1025 while state
1026 .frames
1027 .front()
1028 .map(|(idx, _)| *idx < keep_from)
1029 .unwrap_or(false)
1030 {
1031 state.frames.pop_front();
1032 }
1033 }
1034 while state.frames.len() > MPEG2_STREAM_FRAME_KEEP {
1035 state.frames.pop_front();
1036 }
1037
1038 Ok(())
1039}
1040
1041fn spawn_omv_stream_state(path: PathBuf) -> Result<OmvStreamState> {
1042 let (tx, rx) = mpsc::sync_channel(OMV_STREAM_CHANNEL_CAPACITY);
1043 let request_frames = Arc::new(AtomicUsize::new(OMV_STREAM_DECODE_LEAD_FRAMES));
1044 let worker_request_frames = request_frames.clone();
1045 thread::spawn(move || {
1046 let result = stream_omv_video_worker(path.as_path(), tx.clone(), worker_request_frames);
1047 if let Err(err) = result {
1048 let _ = tx.send(Err(format!("{:#}", err)));
1049 }
1050 });
1051 Ok(OmvStreamState {
1052 rx,
1053 frames: VecDeque::new(),
1054 width: None,
1055 height: None,
1056 fps: None,
1057 frame_time_ms: None,
1058 total_frames_hint: None,
1059 decoded_frames: 0,
1060 done: false,
1061 request_frames,
1062 })
1063}
1064
1065fn wait_for_omv_frame_request(request_frames: &Arc<AtomicUsize>, frame_idx: usize) {
1066 while frame_idx > request_frames.load(Ordering::Acquire) {
1067 if request_frames.load(Ordering::Acquire) == usize::MAX {
1068 return;
1069 }
1070 thread::sleep(Duration::from_millis(1));
1071 }
1072}
1073
1074fn stream_omv_video_worker(
1075 path: &Path,
1076 tx: mpsc::SyncSender<Result<OmvStreamEvent, String>>,
1077 request_frames: Arc<AtomicUsize>,
1078) -> Result<()> {
1079 let omv = siglus_assets::omv::OmvFile::open(path).ok();
1080 let ogg_data = siglus_assets::omv::OmvFile::read_embedded_ogg(path)
1081 .or_else(|_| extract_ogg_by_scan(path))
1082 .with_context(|| format!("read embedded ogg: {}", path.display()))?;
1083 let mut video_tf = siglus_omv_decoder::TheoraFile::open_from_memory(ogg_data)
1084 .with_context(|| format!("open theora: {}", path.display()))?;
1085 let vinfo = video_tf.info();
1086
1087 let display_w = omv
1088 .as_ref()
1089 .map(|m| m.header.display_width as i32)
1090 .unwrap_or(vinfo.width);
1091 let display_h = omv
1092 .as_ref()
1093 .map(|m| m.header.display_height as i32)
1094 .unwrap_or(vinfo.height);
1095 let width = display_w.max(1) as u32;
1096 let height = display_h.max(1) as u32;
1097 let fps = if let Some(m) = omv.as_ref() {
1098 if m.header.frame_time_us != 0 {
1099 Some(1_000_000.0 / (m.header.frame_time_us as f32))
1100 } else if vinfo.fps > 0.0 {
1101 Some(vinfo.fps as f32)
1102 } else {
1103 None
1104 }
1105 } else if vinfo.fps > 0.0 {
1106 Some(vinfo.fps as f32)
1107 } else {
1108 None
1109 };
1110 let frame_time_ms = omv_frame_duration_ms(omv.as_ref().map(|m| &m.header), fps);
1111 let total_frames_hint = omv
1112 .as_ref()
1113 .and_then(|m| (m.header.packet_count_hint > 0).then_some(m.header.packet_count_hint as usize));
1114 let theora_type = omv
1115 .as_ref()
1116 .map(|m| m.header.theora_type)
1117 .unwrap_or(siglus_assets::omv::OMV_THEORA_TYPE_YUV);
1118
1119 if tx
1120 .send(Ok(OmvStreamEvent::Info {
1121 width,
1122 height,
1123 fps,
1124 frame_time_ms,
1125 total_frames_hint,
1126 }))
1127 .is_err()
1128 {
1129 return Ok(());
1130 }
1131
1132 let (_uv_w, _uv_h, y_len, u_len, v_len) =
1133 omv_plane_layout(vinfo.width, vinfo.height, theora_type, vinfo.fmt);
1134 let mut buf = vec![0u8; y_len.saturating_add(u_len).saturating_add(v_len)];
1135 let mut frame_idx = 0usize;
1136 while video_tf.read_video_frame(&mut buf)? {
1137 wait_for_omv_frame_request(&request_frames, frame_idx);
1138 if request_frames.load(Ordering::Acquire) == usize::MAX {
1139 return Ok(());
1140 }
1141 let rgba = convert_omv_frame(
1142 &buf,
1143 vinfo.width,
1144 vinfo.height,
1145 vinfo.fmt,
1146 display_h,
1147 theora_type,
1148 );
1149 let frame = Arc::new(RgbaImage { width, height, center_x: 0, center_y: 0, rgba });
1150 if tx.send(Ok(OmvStreamEvent::Video { frame_idx, frame })).is_err() {
1151 return Ok(());
1152 }
1153 frame_idx = frame_idx.saturating_add(1);
1154 }
1155
1156 let _ = tx.send(Ok(OmvStreamEvent::Done));
1157 Ok(())
1158}
1159
1160fn select_stream_frame(
1161 frames: &VecDeque<(usize, Arc<RgbaImage>)>,
1162 chosen_idx: usize,
1163) -> Option<(usize, Arc<RgbaImage>)> {
1164 let (front_idx, _) = frames.front()?;
1165 let (back_idx, back_frame) = frames.back()?;
1166 if chosen_idx >= *back_idx || chosen_idx < *front_idx {
1167 return Some((*back_idx, back_frame.clone()));
1168 }
1169
1170 let direct = chosen_idx.saturating_sub(*front_idx);
1171 if let Some((idx, frame)) = frames.get(direct) {
1172 if *idx == chosen_idx {
1173 return Some((*idx, frame.clone()));
1174 }
1175 }
1176
1177 let mut i = direct.min(frames.len().saturating_sub(1));
1178 loop {
1179 if let Some((idx, frame)) = frames.get(i) {
1180 if *idx <= chosen_idx {
1181 return Some((*idx, frame.clone()));
1182 }
1183 }
1184 if i == 0 {
1185 break;
1186 }
1187 i -= 1;
1188 }
1189
1190 Some((*back_idx, back_frame.clone()))
1191}
1192
1193fn drain_omv_stream_state(
1194 path: &Path,
1195 state: &mut OmvStreamState,
1196 target_frame_idx: Option<usize>,
1197 retain_from_start: bool,
1198) -> Result<()> {
1199 let decode_until = target_frame_idx
1200 .map(|idx| idx.saturating_add(OMV_STREAM_DECODE_LEAD_FRAMES));
1201
1202 for _ in 0..OMV_STREAM_MAX_DRAIN_EVENTS {
1203 if let Some(limit) = decode_until {
1204 if state.decoded_frames > limit && !state.frames.is_empty() {
1205 break;
1206 }
1207 }
1208 match state.rx.try_recv() {
1209 Ok(Ok(OmvStreamEvent::Info { width, height, fps, frame_time_ms, total_frames_hint })) => {
1210 state.width = Some(width);
1211 state.height = Some(height);
1212 state.fps = fps.or(state.fps);
1213 state.frame_time_ms = frame_time_ms.or(state.frame_time_ms);
1214 state.total_frames_hint = total_frames_hint.or(state.total_frames_hint);
1215 }
1216 Ok(Ok(OmvStreamEvent::Video { frame_idx, frame })) => {
1217 state.decoded_frames = state.decoded_frames.max(frame_idx.saturating_add(1));
1218 state.frames.push_back((frame_idx, frame));
1219 }
1220 Ok(Ok(OmvStreamEvent::Done)) => {
1221 state.done = true;
1222 break;
1223 }
1224 Ok(Err(err)) => {
1225 return Err(anyhow!("omv stream decode failed for {}: {}", path.display(), err));
1226 }
1227 Err(TryRecvError::Empty) => break,
1228 Err(TryRecvError::Disconnected) => {
1229 state.done = true;
1230 break;
1231 }
1232 }
1233 }
1234
1235 if !retain_from_start {
1236 if let Some(target) = target_frame_idx {
1237 let keep_from = target.saturating_sub(2);
1238 while state
1239 .frames
1240 .front()
1241 .map(|(idx, _)| *idx < keep_from)
1242 .unwrap_or(false)
1243 {
1244 state.frames.pop_front();
1245 }
1246 }
1247 while state.frames.len() > OMV_STREAM_FRAME_KEEP {
1248 state.frames.pop_front();
1249 }
1250 }
1251 Ok(())
1252}
1253
1254fn omv_frame_duration_ms(
1255 header: Option<&siglus_assets::omv::OmvHeader>,
1256 fps: Option<f32>,
1257) -> Option<f64> {
1258 if let Some(h) = header {
1259 if h.frame_time_us != 0 {
1260 return Some((h.frame_time_us as f64) / 1000.0);
1261 }
1262 }
1263 let f = fps?;
1264 if f > 0.0 {
1265 Some(1000.0 / (f as f64))
1266 } else {
1267 None
1268 }
1269}
1270
1271fn omv_plane_layout(
1272 width: i32,
1273 video_height: i32,
1274 theora_type: u32,
1275 fmt: i32,
1276) -> (usize, usize, usize, usize, usize) {
1277 let w = width.max(1) as usize;
1278 let vh = video_height.max(1) as usize;
1279 match theora_type {
1280 siglus_assets::omv::OMV_THEORA_TYPE_RGB | siglus_assets::omv::OMV_THEORA_TYPE_RGBA => {
1281 let plane_len = w.saturating_mul(vh);
1285 (w, vh, plane_len, plane_len, plane_len)
1286 }
1287 _ => {
1288 let y_len = w.saturating_mul(vh);
1289 let (uv_w, uv_h) = yuv_plane_size(width, video_height, fmt);
1290 let uv_len = uv_w.saturating_mul(uv_h);
1291 (uv_w, uv_h, y_len, uv_len, uv_len)
1292 }
1293 }
1294}
1295
1296#[derive(Debug, Clone)]
1297pub struct MovieAsset {
1298 pub info: MovieInfo,
1299 pub frames: Vec<Arc<RgbaImage>>,
1300 pub audio: Option<MovieAudio>,
1301}
1302
1303#[derive(Debug, Clone)]
1304pub struct MovieAudio {
1305 pub samples: Arc<Vec<i16>>,
1306 pub channels: u16,
1307 pub sample_rate: u32,
1308 pub start_ms: u64,
1309 pub duration_ms: Option<u64>,
1310}
1311
1312impl MovieAudio {
1313 fn end_ms(&self) -> u64 {
1314 self.start_ms.saturating_add(self.duration_ms.unwrap_or(0))
1315 }
1316}
1317
1318#[derive(Debug)]
1319struct MoviePlayback {
1320 handle: StaticSoundHandle,
1321 started_at: Instant,
1322 duration_ms: Option<u64>,
1323}
1324
1325
1326#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1327fn read_movie_bytes(path: &Path) -> Result<Vec<u8>> {
1328 crate::resource::read_file_bytes(path)
1329 .with_context(|| format!("read movie file: {}", path.display()))
1330}
1331
1332#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1333fn read_movie_bytes(path: &Path) -> Result<Vec<u8>> {
1334 fs::read(path).with_context(|| format!("read movie file: {}", path.display()))
1335}
1336
1337#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1338fn read_omv_header_for_path(path: &Path) -> Result<siglus_assets::omv::OmvHeader> {
1339 let bytes = read_movie_bytes(path)?;
1340 read_omv_header_from_bytes(&bytes)
1341}
1342
1343#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1344fn read_omv_header_for_path(path: &Path) -> Result<siglus_assets::omv::OmvHeader> {
1345 Ok(siglus_assets::omv::OmvFile::open(path)?.header)
1346}
1347
1348fn read_omv_header_from_bytes(buf: &[u8]) -> Result<siglus_assets::omv::OmvHeader> {
1349 if buf.len() < 0x58 {
1350 bail!("OMV header too small");
1351 }
1352 let header_size = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
1353 let version = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
1354 let theora_type = u32::from_le_bytes([buf[0x28], buf[0x29], buf[0x2a], buf[0x2b]]);
1355 let display_width = u32::from_le_bytes([buf[0x2c], buf[0x2d], buf[0x2e], buf[0x2f]]);
1356 let display_height = u32::from_le_bytes([buf[0x30], buf[0x31], buf[0x32], buf[0x33]]);
1357 let frame_time_us = u32::from_le_bytes([buf[0x3c], buf[0x3d], buf[0x3e], buf[0x3f]]);
1358 let max_data_size = u32::from_le_bytes([buf[0x40], buf[0x41], buf[0x42], buf[0x43]]);
1359 let page_count_hint = u32::from_le_bytes([buf[0x4c], buf[0x4d], buf[0x4e], buf[0x4f]]);
1360 let packet_count_hint = u32::from_le_bytes([buf[0x50], buf[0x51], buf[0x52], buf[0x53]]);
1361 if header_size < 0x58 {
1362 bail!("invalid OMV header size: {header_size:#x}");
1363 }
1364 if theora_type > siglus_assets::omv::OMV_THEORA_TYPE_YUV {
1365 bail!("invalid OMV theora type: {theora_type}");
1366 }
1367 if display_width == 0 || display_height == 0 {
1368 bail!("invalid OMV display size: {}x{}", display_width, display_height);
1369 }
1370 Ok(siglus_assets::omv::OmvHeader {
1371 header_size,
1372 version,
1373 theora_type,
1374 display_width,
1375 display_height,
1376 frame_time_us,
1377 max_data_size,
1378 page_count_hint,
1379 packet_count_hint,
1380 })
1381}
1382
1383fn extract_ogg_from_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
1384 let needle = b"OggS";
1385 let pos = bytes
1386 .windows(needle.len())
1387 .position(|w| w == needle)
1388 .ok_or_else(|| anyhow!("OggS not found in OMV payload"))?;
1389 Ok(bytes[pos..].to_vec())
1390}
1391
1392#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1393fn decode_mpeg2_asset_from_bytes(path: &Path, bytes: Vec<u8>) -> Result<MovieAsset> {
1394 let mut width = None;
1395 let mut height = None;
1396 let mut fps = None;
1397 if let Some(h) = siglus_assets::mpeg2::find_sequence_header(&bytes[..bytes.len().min(MPEG2_HEADER_PROBE_BYTES)]) {
1398 width = Some(h.width as u32);
1399 height = Some(h.height as u32);
1400 fps = siglus_assets::mpeg2::fps_from_frame_rate_code(h.frame_rate_code);
1401 }
1402
1403 let mut frames: Vec<Arc<RgbaImage>> = Vec::new();
1404 let mut audio_samples: Vec<i16> = Vec::new();
1405 let mut audio_channels: Option<u16> = None;
1406 let mut audio_sample_rate: Option<u32> = None;
1407 let mut dropped_audio_format_changes = 0u32;
1408 let mut pipeline = na_mpeg2_decoder::MpegAvPipeline::new();
1409 pipeline
1410 .push_with(&bytes, None, |ev| match ev {
1411 na_mpeg2_decoder::MpegAvEvent::Video(f) => {
1412 let w = f.width;
1413 let h = f.height;
1414 frames.push(Arc::new(RgbaImage { width: w, height: h, center_x: 0, center_y: 0, rgba: f.rgba }));
1415 }
1416 na_mpeg2_decoder::MpegAvEvent::Audio(a) => {
1417 append_mpeg2_audio_chunk_for_asset(
1418 path,
1419 &mut audio_channels,
1420 &mut audio_sample_rate,
1421 &mut audio_samples,
1422 &mut dropped_audio_format_changes,
1423 a,
1424 );
1425 }
1426 })
1427 .context("mpeg2 wasm full decode")?;
1428 pipeline.flush_with(|ev| match ev {
1429 na_mpeg2_decoder::MpegAvEvent::Video(f) => {
1430 let w = f.width;
1431 let h = f.height;
1432 frames.push(Arc::new(RgbaImage { width: w, height: h, center_x: 0, center_y: 0, rgba: f.rgba }));
1433 }
1434 na_mpeg2_decoder::MpegAvEvent::Audio(a) => {
1435 append_mpeg2_audio_chunk_for_asset(
1436 path,
1437 &mut audio_channels,
1438 &mut audio_sample_rate,
1439 &mut audio_samples,
1440 &mut dropped_audio_format_changes,
1441 a,
1442 );
1443 }
1444 })?;
1445
1446 if frames.is_empty() {
1447 bail!("mpeg2 decoder produced no frames: {}", path.display());
1448 }
1449 let audio = build_movie_audio_from_parts(path, audio_samples, audio_channels, audio_sample_rate)?;
1450 let audio_duration_ms = audio.as_ref().and_then(|a| a.duration_ms);
1451 let first = frames.first().expect("frames not empty");
1452 let info = MovieInfo {
1453 path: path.to_path_buf(),
1454 width: width.or(Some(first.width)),
1455 height: height.or(Some(first.height)),
1456 fps,
1457 decoded_frames: Some(frames.len()),
1458 audio_duration_ms,
1459 };
1460 Ok(MovieAsset { info, frames, audio })
1461}
1462
1463#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1464fn decode_omv_asset_from_bytes(path: &Path, bytes: Vec<u8>) -> Result<MovieAsset> {
1465 let header = read_omv_header_from_bytes(&bytes).ok();
1466 let ogg_data = extract_ogg_from_bytes(&bytes)
1467 .with_context(|| format!("read embedded ogg: {}", path.display()))?;
1468 let mut tf = siglus_omv_decoder::TheoraFile::open_from_memory(ogg_data)
1469 .with_context(|| format!("open theora: {}", path.display()))?;
1470 let vinfo = tf.info();
1471 let display_w = header.as_ref().map(|h| h.display_width as i32).unwrap_or(vinfo.width);
1472 let display_h = header.as_ref().map(|h| h.display_height as i32).unwrap_or(vinfo.height);
1473 let width = display_w.max(1) as u32;
1474 let height = display_h.max(1) as u32;
1475 let fps = header.as_ref().and_then(|h| {
1476 if h.frame_time_us != 0 {
1477 Some(1_000_000.0 / (h.frame_time_us as f32))
1478 } else if vinfo.fps > 0.0 {
1479 Some(vinfo.fps as f32)
1480 } else {
1481 None
1482 }
1483 }).or_else(|| (vinfo.fps > 0.0).then_some(vinfo.fps as f32));
1484 let theora_type = header
1485 .as_ref()
1486 .map(|h| h.theora_type)
1487 .unwrap_or(siglus_assets::omv::OMV_THEORA_TYPE_YUV);
1488 let (_uv_w, _uv_h, y_len, u_len, v_len) =
1489 omv_plane_layout(vinfo.width, vinfo.height, theora_type, vinfo.fmt);
1490 let mut packed = vec![0u8; y_len.saturating_add(u_len).saturating_add(v_len)];
1491 let mut frames = Vec::<Arc<RgbaImage>>::new();
1492 while tf.read_video_frame(&mut packed)? {
1493 let rgba = convert_omv_frame(
1494 &packed,
1495 vinfo.width,
1496 vinfo.height,
1497 vinfo.fmt,
1498 display_h,
1499 theora_type,
1500 );
1501 frames.push(Arc::new(RgbaImage { width, height, center_x: 0, center_y: 0, rgba }));
1502 }
1503 if frames.is_empty() {
1504 bail!("omv decoder produced no frames: {}", path.display());
1505 }
1506 tf.reset();
1507 let audio = decode_omv_audio(&mut tf)?;
1508 let frame_time_ms = omv_frame_duration_ms(header.as_ref(), fps);
1509 let video_duration_ms = frame_time_ms.map(|ms| ((frames.len() as f64) * ms).round().max(1.0) as u64);
1510 let audio_duration_ms = audio.as_ref().and_then(|a| a.duration_ms).or(video_duration_ms);
1511 let info = MovieInfo {
1512 path: path.to_path_buf(),
1513 width: Some(width),
1514 height: Some(height),
1515 fps,
1516 decoded_frames: Some(frames.len()),
1517 audio_duration_ms,
1518 };
1519 Ok(MovieAsset { info, frames, audio })
1520}
1521
1522#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1523fn append_mpeg2_audio_chunk_for_asset(
1524 _path: &Path,
1525 audio_channels: &mut Option<u16>,
1526 audio_sample_rate: &mut Option<u32>,
1527 audio_samples: &mut Vec<i16>,
1528 dropped_audio_format_changes: &mut u32,
1529 a: na_mpeg2_decoder::MpegAudioF32,
1530) {
1531 match (*audio_channels, *audio_sample_rate) {
1532 (None, None) => {
1533 *audio_channels = Some(a.channels);
1534 *audio_sample_rate = Some(a.sample_rate);
1535 }
1536 (Some(ch), Some(sr)) if ch == a.channels && sr == a.sample_rate => {}
1537 (Some(_), Some(_)) => {
1538 *dropped_audio_format_changes = (*dropped_audio_format_changes).saturating_add(1);
1539 return;
1540 }
1541 _ => return,
1542 }
1543 audio_samples.extend(a.samples.into_iter().map(f32_to_i16_sample));
1544}
1545
1546#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1547fn build_movie_audio_from_parts(
1548 path: &Path,
1549 audio_samples: Vec<i16>,
1550 audio_channels: Option<u16>,
1551 audio_sample_rate: Option<u32>,
1552) -> Result<Option<MovieAudio>> {
1553 match (audio_channels, audio_sample_rate, audio_samples.is_empty()) {
1554 (Some(channels), Some(sample_rate), false) => {
1555 if channels == 0 || sample_rate == 0 {
1556 bail!(
1557 "movie audio stream has invalid format in {}: channels={} sample_rate={}",
1558 path.display(), channels, sample_rate
1559 );
1560 }
1561 let frames_len = (audio_samples.len() as u64) / (channels as u64);
1562 let duration_ms = Some(((frames_len as f64) * 1000.0 / sample_rate as f64).round() as u64);
1563 Ok(Some(MovieAudio {
1564 samples: Arc::new(audio_samples),
1565 channels,
1566 sample_rate,
1567 start_ms: 0,
1568 duration_ms,
1569 }))
1570 }
1571 (None, None, true) => Ok(None),
1572 (Some(_), Some(_), true) => Ok(None),
1573 _ => bail!("movie audio decoder produced incomplete format metadata for {}", path.display()),
1574 }
1575}
1576
1577fn decode_asset_for_path(path: &Path) -> Result<MovieAsset> {
1578 let ext = path
1579 .extension()
1580 .and_then(|s| s.to_str())
1581 .unwrap_or("")
1582 .to_ascii_lowercase();
1583 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1584 {
1585 let bytes = read_movie_bytes(path)?;
1586 if ext == "omv" {
1587 return decode_omv_asset_from_bytes(path, bytes);
1588 }
1589 return decode_mpeg2_asset_from_bytes(path, bytes);
1590 }
1591 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1592 {
1593 if ext == "omv" {
1594 decode_omv_asset(path)
1595 } else {
1596 decode_mpeg2_asset(path)
1597 }
1598 }
1599}
1600
1601fn decode_mpeg2_preview_frame(path: &Path) -> Result<Arc<RgbaImage>> {
1602 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1603 {
1604 let bytes = read_movie_bytes(path)?;
1605 let asset = decode_mpeg2_asset_from_bytes(path, bytes)?;
1606 return asset
1607 .frames
1608 .into_iter()
1609 .next()
1610 .ok_or_else(|| anyhow!("mpeg2 preview frame missing: {}", path.display()));
1611 }
1612
1613 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1614 {
1615 let mut file = fs::File::open(path).with_context(|| format!("open movie file: {}", path.display()))?;
1616 let mut pipeline = na_mpeg2_decoder::MpegVideoPipeline::new();
1617 let mut first = None;
1618 let mut buf = vec![0u8; MPEG2_STREAM_CHUNK_BYTES];
1619 loop {
1620 let n = file
1621 .read(&mut buf)
1622 .with_context(|| format!("read movie preview stream: {}", path.display()))?;
1623 if n == 0 {
1624 break;
1625 }
1626 pipeline
1627 .push_with(&buf[..n], None, |f| {
1628 if first.is_none() {
1629 let w = f.width as u32;
1630 let h = f.height as u32;
1631 let mut rgba = vec![0u8; (w as usize) * (h as usize) * 4];
1632 na_mpeg2_decoder::frame_to_rgba_bt601_limited(&f, &mut rgba);
1633 first = Some(Arc::new(RgbaImage {
1634 width: w,
1635 height: h,
1636 center_x: 0,
1637 center_y: 0,
1638 rgba,
1639 }));
1640 }
1641 })
1642 .context("mpeg2 preview decode")?;
1643 if first.is_some() {
1644 break;
1645 }
1646 }
1647 if first.is_none() {
1648 pipeline.flush_with(|f| {
1649 if first.is_none() {
1650 let w = f.width as u32;
1651 let h = f.height as u32;
1652 let mut rgba = vec![0u8; (w as usize) * (h as usize) * 4];
1653 na_mpeg2_decoder::frame_to_rgba_bt601_limited(&f, &mut rgba);
1654 first = Some(Arc::new(RgbaImage {
1655 width: w,
1656 height: h,
1657 center_x: 0,
1658 center_y: 0,
1659 rgba,
1660 }));
1661 }
1662 })?;
1663 }
1664 first.ok_or_else(|| anyhow!("mpeg2 preview frame missing: {}", path.display()))
1665 }
1666}
1667
1668fn decode_omv_preview_frame(path: &Path) -> Result<Arc<RgbaImage>> {
1669 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1670 {
1671 let bytes = read_movie_bytes(path)?;
1672 let asset = decode_omv_asset_from_bytes(path, bytes)?;
1673 return asset
1674 .frames
1675 .into_iter()
1676 .next()
1677 .ok_or_else(|| anyhow!("omv preview frame missing: {}", path.display()));
1678 }
1679
1680 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1681 {
1682 let omv = siglus_assets::omv::OmvFile::open(path).ok();
1683 let ogg_data = siglus_assets::omv::OmvFile::read_embedded_ogg(path)
1684 .or_else(|_| extract_ogg_by_scan(path))
1685 .with_context(|| format!("read embedded ogg: {}", path.display()))?;
1686 let (vinfo, packed) = siglus_omv_decoder::decode_first_video_frame_from_memory(ogg_data)
1687 .with_context(|| format!("decode first omv frame: {}", path.display()))?;
1688 let display_h = omv
1689 .as_ref()
1690 .map(|m| m.header.display_height as i32)
1691 .unwrap_or(vinfo.height);
1692 let width = omv
1693 .as_ref()
1694 .map(|m| m.header.display_width.max(1))
1695 .unwrap_or(vinfo.width.max(1) as u32);
1696 let height = display_h.max(1) as u32;
1697 let rgba = convert_omv_frame(
1698 &packed,
1699 vinfo.width,
1700 vinfo.height,
1701 vinfo.fmt,
1702 display_h,
1703 omv.as_ref()
1704 .map(|m| m.header.theora_type)
1705 .unwrap_or(siglus_assets::omv::OMV_THEORA_TYPE_YUV),
1706 );
1707 Ok(Arc::new(RgbaImage {
1708 width,
1709 height,
1710 center_x: 0,
1711 center_y: 0,
1712 rgba,
1713 }))
1714 }
1715}
1716
1717fn decode_mpeg2_audio_for_path(path: &Path) -> Result<Option<MovieAudio>> {
1718 let mut audio_samples: Vec<i16> = Vec::new();
1719 let mut audio_channels: Option<u16> = None;
1720 let mut audio_sample_rate: Option<u32> = None;
1721 let mut dropped_audio_format_changes: u32 = 0;
1722
1723 fn append_chunk(
1724 path: &Path,
1725 phase: &str,
1726 audio_channels: &mut Option<u16>,
1727 audio_sample_rate: &mut Option<u32>,
1728 audio_samples: &mut Vec<i16>,
1729 dropped_audio_format_changes: &mut u32,
1730 a: na_mpeg2_decoder::MpegAudioF32,
1731 ) {
1732 match (*audio_channels, *audio_sample_rate) {
1733 (None, None) => {
1734 *audio_channels = Some(a.channels);
1735 *audio_sample_rate = Some(a.sample_rate);
1736 }
1737 (Some(ch), Some(sr)) if ch == a.channels && sr == a.sample_rate => {}
1738 (Some(ch), Some(sr)) => {
1739 *dropped_audio_format_changes = (*dropped_audio_format_changes).saturating_add(1);
1740 if std::env::var_os("SG_MOVIE_TRACE").is_some()
1741 || std::env::var_os("SG_DEBUG").is_some()
1742 {
1743 eprintln!(
1744 "[SG_DEBUG][MOV] mpeg2_audio_format_change.drop phase={} path={} base={}ch/{}Hz got={}ch/{}Hz samples={}",
1745 phase,
1746 path.display(),
1747 ch,
1748 sr,
1749 a.channels,
1750 a.sample_rate,
1751 a.samples.len()
1752 );
1753 }
1754 return;
1755 }
1756 _ => return,
1757 }
1758 audio_samples.extend(a.samples.into_iter().map(f32_to_i16_sample));
1759 }
1760
1761 let mut file = fs::File::open(path).with_context(|| format!("open movie file: {}", path.display()))?;
1762 let mut pipeline = na_mpeg2_decoder::MpegAvPipeline::new();
1763 let mut buf = vec![0u8; MPEG2_STREAM_CHUNK_BYTES];
1764
1765 loop {
1766 let n = file
1767 .read(&mut buf)
1768 .with_context(|| format!("read movie audio stream: {}", path.display()))?;
1769 if n == 0 {
1770 break;
1771 }
1772 pipeline
1773 .push_with(&buf[..n], None, |ev| {
1774 if let na_mpeg2_decoder::MpegAvEvent::Audio(a) = ev {
1775 append_chunk(
1776 path,
1777 "decode",
1778 &mut audio_channels,
1779 &mut audio_sample_rate,
1780 &mut audio_samples,
1781 &mut dropped_audio_format_changes,
1782 a,
1783 );
1784 }
1785 })
1786 .context("mpeg2 audio decode")?;
1787 }
1788
1789 pipeline.flush_with(|ev| {
1790 if let na_mpeg2_decoder::MpegAvEvent::Audio(a) = ev {
1791 append_chunk(
1792 path,
1793 "flush",
1794 &mut audio_channels,
1795 &mut audio_sample_rate,
1796 &mut audio_samples,
1797 &mut dropped_audio_format_changes,
1798 a,
1799 );
1800 }
1801 })?;
1802
1803 match (audio_channels, audio_sample_rate, audio_samples.is_empty()) {
1804 (Some(channels), Some(sample_rate), false) => {
1805 if channels == 0 || sample_rate == 0 {
1806 bail!(
1807 "mpeg2 audio stream has invalid format in {}: channels={} sample_rate={}",
1808 path.display(), channels, sample_rate
1809 );
1810 }
1811 let frames_len = (audio_samples.len() as u64) / (channels as u64);
1812 let duration_ms = Some(
1813 ((frames_len as f64) * 1000.0 / sample_rate as f64).round() as u64,
1814 );
1815 Ok(Some(MovieAudio {
1816 samples: Arc::new(audio_samples),
1817 channels,
1818 sample_rate,
1819 start_ms: 0,
1820 duration_ms,
1821 }))
1822 }
1823 (None, None, true) => Ok(None),
1824 (Some(_), Some(_), true) => Ok(None),
1825 _ => bail!(
1826 "mpeg2 audio decoder produced incomplete format metadata for {}",
1827 path.display()
1828 ),
1829 }
1830}
1831
1832fn decode_mpeg2_asset(path: &Path) -> Result<MovieAsset> {
1833 let prefix = read_file_prefix(path, MPEG2_HEADER_PROBE_BYTES)
1834 .with_context(|| format!("read movie header: {}", path.display()))?;
1835 let mut width = None;
1836 let mut height = None;
1837 let mut fps = None;
1838 if let Some(h) = siglus_assets::mpeg2::find_sequence_header(&prefix) {
1839 width = Some(h.width as u32);
1840 height = Some(h.height as u32);
1841 fps = siglus_assets::mpeg2::fps_from_frame_rate_code(h.frame_rate_code);
1842 }
1843 let frame = decode_mpeg2_preview_frame(path)?;
1844 let info = MovieInfo {
1845 path: path.to_path_buf(),
1846 width: width.or(Some(frame.width)),
1847 height: height.or(Some(frame.height)),
1848 fps,
1849 decoded_frames: Some(1),
1850 audio_duration_ms: None,
1851 };
1852 Ok(MovieAsset {
1853 info,
1854 frames: vec![frame],
1855 audio: None,
1856 })
1857}
1858
1859fn f32_to_i16_sample(s: f32) -> i16 {
1860 let clamped = s.max(-1.0).min(1.0);
1861 (clamped * 32767.0).round() as i16
1862}
1863
1864fn decode_omv_asset(path: &Path) -> Result<MovieAsset> {
1865 let omv = siglus_assets::omv::OmvFile::open(path).ok();
1866 let frame = decode_omv_preview_frame(path)?;
1867 let fps = omv.as_ref().and_then(|m| {
1868 if m.header.frame_time_us != 0 {
1869 Some(1_000_000.0 / (m.header.frame_time_us as f32))
1870 } else {
1871 None
1872 }
1873 });
1874 let decoded_frames = omv
1875 .as_ref()
1876 .and_then(|m| (m.header.packet_count_hint > 0).then_some(m.header.packet_count_hint as usize))
1877 .or(Some(1));
1878 let audio_duration_ms = decoded_frames.and_then(|frames| {
1879 omv_frame_duration_ms(omv.as_ref().map(|m| &m.header), fps)
1880 .map(|ms| ((frames as f64) * ms).round().max(1.0) as u64)
1881 });
1882 let info = MovieInfo {
1883 path: path.to_path_buf(),
1884 width: Some(frame.width),
1885 height: Some(frame.height),
1886 fps,
1887 decoded_frames,
1888 audio_duration_ms,
1889 };
1890 Ok(MovieAsset {
1891 info,
1892 frames: vec![frame],
1893 audio: None,
1894 })
1895}
1896
1897fn extract_ogg_by_scan(path: &Path) -> Result<Vec<u8>> {
1898 let bytes = read_movie_bytes(path)?;
1899 extract_ogg_from_bytes(&bytes).with_context(|| format!("OggS not found in OMV: {}", path.display()))
1900}
1901
1902fn decode_omv_audio(tf: &mut siglus_omv_decoder::TheoraFile) -> Result<Option<MovieAudio>> {
1903 if !tf.has_audio() {
1904 return Ok(None);
1905 }
1906 let Some((channels, sample_rate)) = tf.audio_info() else {
1907 return Ok(None);
1908 };
1909 if channels <= 0 || sample_rate <= 0 {
1910 return Ok(None);
1911 }
1912 let channels_u16 = channels as u16;
1913 let sample_rate_u32 = sample_rate as u32;
1914
1915 let mut samples: Vec<f32> = Vec::new();
1916 let mut buf = vec![0.0f32; (4096usize).saturating_mul(channels as usize)];
1917 loop {
1918 let read = tf.read_audio_samples(&mut buf)?;
1919 if read == 0 {
1920 break;
1921 }
1922 samples.extend_from_slice(&buf[..read]);
1923 }
1924
1925 if samples.is_empty() {
1926 return Ok(None);
1927 }
1928 let mut samples_i16: Vec<i16> = Vec::with_capacity(samples.len());
1929 for &s in &samples {
1930 let clamped = s.max(-1.0).min(1.0);
1931 let v = (clamped * 32767.0).round() as i16;
1932 samples_i16.push(v);
1933 }
1934 let frames = (samples_i16.len() as u64) / (channels_u16 as u64);
1935 let duration_ms = if sample_rate_u32 > 0 {
1936 Some(((frames as f64) * 1000.0 / (sample_rate_u32 as f64)).round() as u64)
1937 } else {
1938 None
1939 };
1940
1941 Ok(Some(MovieAudio {
1942 samples: Arc::new(samples_i16),
1943 channels: channels_u16,
1944 sample_rate: sample_rate_u32,
1945 start_ms: 0,
1946 duration_ms,
1947 }))
1948}
1949
1950fn encode_wav_i16_interleaved_offset(track: &MovieAudio, offset_ms: u64) -> Vec<u8> {
1951 let channels = track.channels;
1952 let sample_rate = track.sample_rate;
1953 let samples = track.samples.as_ref();
1954 let frames_offset = ((offset_ms as u64) * (sample_rate as u64) / 1000) as usize;
1955 let start = frames_offset.saturating_mul(channels as usize);
1956 let slice = if start < samples.len() {
1957 &samples[start..]
1958 } else {
1959 &samples[samples.len()..]
1960 };
1961 encode_wav_i16_interleaved(slice, channels, sample_rate)
1962}
1963
1964fn encode_wav_i16_interleaved(samples: &[i16], channels: u16, sample_rate: u32) -> Vec<u8> {
1965 let bytes_per_sample = 2u16;
1966 let block_align = channels.saturating_mul(bytes_per_sample);
1967 let byte_rate = (sample_rate as u64).saturating_mul(block_align as u64) as u32;
1968 let data_bytes = samples.len().saturating_mul(bytes_per_sample as usize) as u32;
1969 let riff_size = 36u32.saturating_add(data_bytes);
1970
1971 let mut out = Vec::with_capacity((data_bytes as usize) + 44);
1972 out.extend_from_slice(b"RIFF");
1973 out.extend_from_slice(&riff_size.to_le_bytes());
1974 out.extend_from_slice(b"WAVE");
1975 out.extend_from_slice(b"fmt ");
1976 out.extend_from_slice(&16u32.to_le_bytes());
1977 out.extend_from_slice(&1u16.to_le_bytes());
1978 out.extend_from_slice(&channels.to_le_bytes());
1979 out.extend_from_slice(&sample_rate.to_le_bytes());
1980 out.extend_from_slice(&byte_rate.to_le_bytes());
1981 out.extend_from_slice(&block_align.to_le_bytes());
1982 out.extend_from_slice(&16u16.to_le_bytes());
1983 out.extend_from_slice(b"data");
1984 out.extend_from_slice(&data_bytes.to_le_bytes());
1985
1986 for &s in samples {
1987 out.extend_from_slice(&s.to_le_bytes());
1988 }
1989 out
1990}
1991
1992fn convert_omv_frame(
1993 data: &[u8],
1994 width: i32,
1995 video_height: i32,
1996 fmt: i32,
1997 display_height: i32,
1998 theora_type: u32,
1999) -> Vec<u8> {
2000 let w = width.max(1) as usize;
2001 let vh = video_height.max(1) as usize;
2002 let dh = display_height.max(1) as usize;
2003
2004 let (uv_w, _uv_h, y_plane_len, u_plane_len, _v_plane_len) =
2005 omv_plane_layout(width, video_height, theora_type, fmt);
2006 let y_off = 0usize;
2007 let u_off = y_off.saturating_add(y_plane_len);
2008 let v_off = u_off.saturating_add(u_plane_len);
2009
2010 let mut rgba = vec![0u8; w.saturating_mul(dh).saturating_mul(4)];
2011
2012 match theora_type {
2013 siglus_assets::omv::OMV_THEORA_TYPE_RGB => {
2014 for y in 0..dh {
2015 for x in 0..w {
2016 let b = get_plane_sample(data, y_off, w, x, y, 0);
2017 let g = get_plane_sample(data, u_off, uv_w, x, y, 0);
2018 let r = get_plane_sample(data, v_off, uv_w, x, y, 0);
2019 let out = (y * w + x) * 4;
2020 rgba[out] = r;
2021 rgba[out + 1] = g;
2022 rgba[out + 2] = b;
2023 rgba[out + 3] = 0xff;
2024 }
2025 }
2026 }
2027 siglus_assets::omv::OMV_THEORA_TYPE_RGBA => {
2028 let alpha_h = (dh + 2) / 3;
2029 let alpha_h_2 = alpha_h * 2;
2030 for y in 0..dh {
2031 let (a_off, local_y, a_width) = if y < alpha_h {
2032 (y_off, y, w)
2033 } else if y < alpha_h_2 {
2034 (u_off, y - alpha_h, uv_w)
2035 } else {
2036 (v_off, y - alpha_h_2, uv_w)
2037 };
2038 let alpha_y = dh.saturating_add(local_y);
2039 for x in 0..w {
2040 let b = get_plane_sample(data, y_off, w, x, y, 0);
2041 let g = get_plane_sample(data, u_off, uv_w, x, y, 0);
2042 let r = get_plane_sample(data, v_off, uv_w, x, y, 0);
2043 let a = get_plane_sample(data, a_off, a_width, x, alpha_y, 0xff);
2044 let out = (y * w + x) * 4;
2045 rgba[out] = r;
2046 rgba[out + 1] = g;
2047 rgba[out + 2] = b;
2048 rgba[out + 3] = a;
2049 }
2050 }
2051 }
2052 _ => {
2053 for y in 0..dh {
2054 let y_row = y * w;
2055 let uv_y = match fmt {
2056 siglus_omv_decoder::TH_PF_420 => y / 2,
2057 _ => y,
2058 };
2059 for x in 0..w {
2060 let y_idx = y_row + x;
2061 let yv = data.get(y_idx).copied().unwrap_or(0) as f32;
2062
2063 let uv_x = match fmt {
2064 siglus_omv_decoder::TH_PF_420 | siglus_omv_decoder::TH_PF_422 => x / 2,
2065 _ => x,
2066 };
2067 let u_idx = u_off
2068 .saturating_add(uv_y.saturating_mul(uv_w))
2069 .saturating_add(uv_x);
2070 let v_idx = v_off
2071 .saturating_add(uv_y.saturating_mul(uv_w))
2072 .saturating_add(uv_x);
2073 let u = data.get(u_idx).copied().unwrap_or(128) as f32 - 128.0;
2074 let v = data.get(v_idx).copied().unwrap_or(128) as f32 - 128.0;
2075
2076 let r = clamp_f(yv + 1.40200 * v);
2077 let g = clamp_f(yv - 0.34414 * u - 0.71414 * v);
2078 let b = clamp_f(yv + 1.77200 * u);
2079
2080 let out = (y * w + x) * 4;
2081 rgba[out] = r;
2082 rgba[out + 1] = g;
2083 rgba[out + 2] = b;
2084 rgba[out + 3] = 0xff;
2085 }
2086 }
2087 }
2088 }
2089
2090 rgba
2091}
2092
2093fn get_plane_sample(
2094 data: &[u8],
2095 plane_off: usize,
2096 plane_width: usize,
2097 x: usize,
2098 y: usize,
2099 default: u8,
2100) -> u8 {
2101 if plane_width == 0 {
2102 return default;
2103 }
2104 data.get(
2105 plane_off
2106 .saturating_add(y.saturating_mul(plane_width))
2107 .saturating_add(x),
2108 )
2109 .copied()
2110 .unwrap_or(default)
2111}
2112
2113fn clamp_f(v: f32) -> u8 {
2114 if v <= 0.0 {
2115 0
2116 } else if v >= 255.0 {
2117 255
2118 } else {
2119 v.round() as u8
2120 }
2121}
2122
2123fn yuv_plane_size(width: i32, height: i32, fmt: i32) -> (usize, usize) {
2124 let w = width.max(1) as usize;
2125 let h = height.max(1) as usize;
2126 match fmt {
2127 siglus_omv_decoder::TH_PF_420 => (w / 2, h / 2),
2128 siglus_omv_decoder::TH_PF_422 => (w / 2, h),
2129 siglus_omv_decoder::TH_PF_444 => (w, h),
2130 _ => (w / 2, h / 2),
2131 }
2132}