Media: a clip opened from a .pptx gets its length and size from its headers (#1241) (#1250)

This commit is contained in:
aiongg
2026-10-23 19:56:24 -04:00
committed by GitHub
parent 7626f74b99
commit 9aef21e627
4 changed files with 71 additions and 8 deletions
+42 -1
View File
@@ -1,6 +1,10 @@
//! File menu: new, open, save, export, close, properties. //! File menu: new, open, save, export, close, properties.
use deckcraft_model::{Presentation, defaults}; use std::collections::HashMap;
use std::sync::Arc;
use deckcraft_media::MediaInfo;
use deckcraft_model::{MediaId, MediaItem, Presentation, Shape, ShapeKind, defaults};
use serde_json::{Value, json}; use serde_json::{Value, json};
use super::*; use super::*;
@@ -156,6 +160,7 @@ fn open_other(name: &str, bytes: &[u8]) -> Result<(Presentation, Vec<String>)> {
if deckcraft_pptx::sniff(bytes) { if deckcraft_pptx::sniff(bytes) {
let (mut p, lost_slides) = deckcraft_pptx::import_checked(bytes).map_err(|e| EngineError::Other(e.to_string()))?; let (mut p, lost_slides) = deckcraft_pptx::import_checked(bytes).map_err(|e| EngineError::Other(e.to_string()))?;
deckcraft_format::repair(&mut p); deckcraft_format::repair(&mut p);
probe_clips(&mut p);
return Ok((p, lost_slides)); return Ok((p, lost_slides));
} }
if deckcraft_pptx::is_encrypted(bytes) { if deckcraft_pptx::is_encrypted(bytes) {
@@ -178,6 +183,42 @@ fn open_other(name: &str, bytes: &[u8]) -> Result<(Presentation, Vec<String>)> {
Err(EngineError::Other(format!("{name}: not a presentation DeckCraft can read"))) Err(EngineError::Other(format!("{name}: not a presentation DeckCraft can read")))
} }
/// Fill in each audio and video clip's length and picture size from its bytes: a `.pptx` doesn't
/// store them. Headers only, so opening never decodes a whole audio file.
fn probe_clips(p: &mut Presentation) {
fn rec(shapes: &mut [Shape], media: &[MediaItem], probed: &mut HashMap<MediaId, Option<MediaInfo>>, depth: usize) {
if depth > 64 {
return;
}
for s in shapes {
if let ShapeKind::Media(m) = &mut s.kind
&& (m.duration_ms == 0 || m.width == 0 && m.height == 0)
&& let Some(info) = probed
.entry(m.media)
.or_insert_with(|| media.iter().find(|i| i.id == m.media).and_then(|i| deckcraft_media::probe_headers(&i.data).ok()))
{
if m.duration_ms == 0 {
m.duration_ms = info.duration_ms.min(u32::MAX as u64) as u32;
}
if m.width == 0
&& m.height == 0
&& let Some(v) = &info.video
{
(m.width, m.height) = (v.width, v.height);
}
}
if let Some(children) = s.children_mut() {
rec(children, media, probed, depth + 1);
}
}
}
let mut probed = HashMap::new();
// Just read, so no slide is shared yet and `make_mut` doesn't copy.
for slide in &mut p.slides {
rec(&mut Arc::make_mut(slide).shapes, &p.media, &mut probed, 0);
}
}
pub fn add_opened(s: &mut Session, name: &str, path: Option<String>, p: Presentation) -> usize { pub fn add_opened(s: &mut Session, name: &str, path: Option<String>, p: Presentation) -> usize {
let title = std::path::Path::new(name).file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| name.to_string()); let title = std::path::Path::new(name).file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| name.to_string());
crate::fonts::install_embedded_fonts(&p); crate::fonts::install_embedded_fonts(&p);
+15
View File
@@ -228,6 +228,21 @@ mod tests {
assert!(s.execute("media.posterFrame", &json!({})).is_err(), "audio has no poster frame"); assert!(s.execute("media.posterFrame", &json!({})).is_err(), "audio has no poster frame");
} }
#[test]
fn a_video_reopened_from_pptx_keeps_its_length_and_size() {
let mut s = Session::with_new();
let clip = include_bytes!("../../../media/tests/fixtures/clip-h264.mp4");
s.execute("insert.video", &json!({"name": "clip.mp4", "data": base64_encode(clip)})).expect("insert");
let before = s.execute("media.info", &json!({})).expect("info");
assert!(before["durationMs"].as_u64().is_some_and(|ms| ms > 0));
let saved = s.execute("file.saveBytes", &json!({"format": "pptx"})).expect("save");
s.execute("file.openBytes", &json!({"name": "deck.pptx", "data": saved["data"]})).expect("open");
let after = s.execute("media.info", &json!({})).expect("info");
for key in ["durationMs", "width", "height"] {
assert_eq!(after[key], before[key], "{key}");
}
}
#[test] #[test]
fn media_commands_need_media() { fn media_commands_need_media() {
let mut s = Session::with_new(); let mut s = Session::with_new();
+1 -1
View File
@@ -31,7 +31,7 @@ pub use audio::Pcm;
pub use clip::ClipParams; pub use clip::ClipParams;
pub use feed::VideoFeed; pub use feed::VideoFeed;
pub use player::{AudioOut, PlayState, Player, VoiceSpec, VoiceStatus}; pub use player::{AudioOut, PlayState, Player, VoiceSpec, VoiceStatus};
pub use probe::{AudioInfo, MediaInfo, VideoInfo, probe}; pub use probe::{AudioInfo, MediaInfo, VideoInfo, probe, probe_headers};
pub use video::{Frame, VideoDecoder, poster_frame, poster_png}; pub use video::{Frame, VideoDecoder, poster_frame, poster_png};
/// Shared media bytes (the model's `MediaItem::data`). /// Shared media bytes (the model's `MediaItem::data`).
+13 -6
View File
@@ -48,6 +48,19 @@ fn ms(secs: f64) -> u64 {
/// Probe `bytes`. /// Probe `bytes`.
pub fn probe(bytes: &Bytes) -> Result<MediaInfo> { pub fn probe(bytes: &Bytes) -> Result<MediaInfo> {
let mut info = probe_headers(bytes)?;
if info.duration_ms == 0 && info.audio.as_ref().is_some_and(|a| a.decodable) && info.video.is_none() {
// No length in the header (raw MP3 / ADTS without an index): decode to measure.
if let Ok(pcm) = crate::audio::decode(bytes) {
info.duration_ms = ms(pcm.duration());
}
}
Ok(info)
}
/// [`probe`] from the headers alone, without decoding: a raw MP3 or ADTS stream without an
/// index reports a `duration_ms` of 0.
pub fn probe_headers(bytes: &Bytes) -> Result<MediaInfo> {
let c = Container::sniff(bytes); let c = Container::sniff(bytes);
let mut info = match c { let mut info = match c {
Container::Mp4 => probe_mp4(bytes)?, Container::Mp4 => probe_mp4(bytes)?,
@@ -73,12 +86,6 @@ pub fn probe(bytes: &Bytes) -> Result<MediaInfo> {
info.duration_ms = ms(n as f64 / rate as f64); info.duration_ms = ms(n as f64 / rate as f64);
} }
} }
if info.duration_ms == 0 && info.audio.as_ref().is_some_and(|a| a.decodable) && info.video.is_none() {
// No length in the header (raw MP3 / ADTS without an index): decode to measure.
if let Ok(pcm) = crate::audio::decode(bytes) {
info.duration_ms = ms(pcm.duration());
}
}
Ok(info) Ok(info)
} }