mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-25 19:17:06 -06:00
Merge pull request #1256
This commit is contained in:
+1
-1
@@ -27,7 +27,7 @@ artwork. The ribbon and menu icons stay original drawings.
|
||||
| `assets/app-icon/icon-source.deckcraft` | DeckCraft contributors | made with `deckcraft-cli run` (see git history) | MIT OR Apache-2.0 | Source of the placeholder icon |
|
||||
| `docs/images/editor.png`, `docs/images/shape-format.png`, `docs/images/format-pane.png`, `docs/images/slide-sorter.png`, `docs/images/dark-mode.png`, `docs/images/sample-deck.png` | DeckCraft contributors | Screenshots of DeckCraft and slides rendered by `deckcraft-cli` from the built-in sample deck (original content; Inter font, OFL-1.1) | MIT OR Apache-2.0 | README screenshots |
|
||||
| `crates/media/tests/fixtures/tone*` (`tone.mp3`, `.m4a`, `.aac`, `.ogg`, `.opus`, `.flac`, `.wma`, `tone-alac.m4a`) | DeckCraft contributors | synthesised 0.5 s 440 Hz sine, encoded by `crates/media/tests/fixtures/make.sh` (ffmpeg as the encoder only) | CC0-1.0 | Audio decoding tests |
|
||||
| `crates/media/tests/fixtures/clip-*` (`clip-h264.mp4`, `clip-h264.mov`, `clip-hevc.mp4`, `clip-vp9.webm`, `clip-av1.mkv`) | DeckCraft contributors | 6 frames of a colour gradient generated by `make.sh` (python3), encoded by ffmpeg (x264 / x265 / libvpx / SVT-AV1 as encoders only) | CC0-1.0 | Video decoding tests |
|
||||
| `crates/media/tests/fixtures/clip-*` (`clip-h264.mp4`, `clip-h264.mov`, `clip-hevc.mp4`, `clip-vp8.webm`, `clip-vp9.webm`, `clip-av1.mkv`) | DeckCraft contributors | 6 frames of a colour gradient generated by `make.sh` (python3), encoded by ffmpeg (x264 / x265 / libvpx / libvpx-vp9 / SVT-AV1 as encoders only) | CC0-1.0 | Video decoding tests |
|
||||
| `crates/media/tests/fixtures/damaged/*` (`ebml-header.mkv`, `ebml-position.webm`, `demux-loop.mp4`, `demux-loop.m4a`) | DeckCraft contributors | the `clip-*` and `tone*` fixtures above with bytes changed by a mutation fuzzer | CC0-1.0 | Damaged media must not panic or hang |
|
||||
| `crates/media/tests/fixtures/damaged/*` (`stsc-count.m4a`, `stsz-size.m4a`) | DeckCraft contributors | `tone.m4a` above with bytes changed by a mutation fuzzer | CC0-1.0 | Damaged MP4 sample tables must not abort |
|
||||
| `crates/media/tests/fixtures/make.sh` | DeckCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures |
|
||||
|
||||
@@ -1281,14 +1281,25 @@ fn media(s: &mut Session, p: &Value, video: bool) -> Result<Value> {
|
||||
}
|
||||
Err(_) => (0, 0, 0, false),
|
||||
};
|
||||
let warning = (!playable).then(|| match &info {
|
||||
Ok(i) => {
|
||||
let codecs: Vec<&str> = i.audio.iter().map(|a| a.codec.as_str()).chain(i.video.iter().map(|v| v.codec.as_str())).collect();
|
||||
format!("{name} was inserted, but it can't be played ({} isn't supported yet)", codecs.join(" / "))
|
||||
}
|
||||
Err(e) => format!("{name} was inserted, but it can't be played ({e})"),
|
||||
});
|
||||
let decodable_video = info.as_ref().is_ok_and(|i| i.video.as_ref().is_some_and(|v| v.decodable));
|
||||
let warning = if !playable {
|
||||
Some(match &info {
|
||||
Ok(i) => {
|
||||
let codecs: Vec<&str> = i.audio.iter().map(|a| a.codec.as_str()).chain(i.video.iter().map(|v| v.codec.as_str())).collect();
|
||||
format!("{name} was inserted, but it can't be played ({} isn't supported yet)", codecs.join(" / "))
|
||||
}
|
||||
Err(e) => format!("{name} was inserted, but it can't be played ({e})"),
|
||||
})
|
||||
} else if video && !decodable_video {
|
||||
// A video codec without a decoder (VP8, say): the clip's audio still plays, but no frame
|
||||
// can ever show — say so instead of leaving an unexplained black box (#1237).
|
||||
info.as_ref()
|
||||
.ok()
|
||||
.and_then(|i| i.video.as_ref())
|
||||
.map(|v| format!("{name} was inserted, but its video can't be played ({} isn't supported yet); only the audio plays", v.codec))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let poster_png = if video && decodable_video { super::media::poster_png(&bytes, 0) } else { None };
|
||||
let size = s.doc()?.doc.slide_size;
|
||||
let rect = rect_param(p, "rect").unwrap_or_else(|| {
|
||||
|
||||
@@ -290,4 +290,16 @@ mod tests {
|
||||
assert_eq!(r["playable"], false);
|
||||
assert!(r["warning"].as_str().is_some_and(|w| w.contains("can't be played")), "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_vp8_webm_is_playable_and_warns_that_only_the_audio_plays() {
|
||||
let mut s = Session::with_new();
|
||||
let _ = s.execute("slide.new", &json!({"layout": "blank"}));
|
||||
let clip = include_bytes!("../../../media/tests/fixtures/clip-vp8.webm");
|
||||
let r = s.execute("insert.video", &json!({"name": "clip.webm", "data": base64_encode(clip)})).expect("insert");
|
||||
assert_eq!(r["playable"], true, "the Vorbis track plays: {r}");
|
||||
let w = r["warning"].as_str().expect("the VP8 track can't be shown");
|
||||
assert!(w.contains("VP8") && w.contains("only the audio plays"), "{w}");
|
||||
assert!(matches!(s.ui_requests.last(), Some(UiRequest::Message { .. })), "the warning reaches the status bar");
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
Vendored
+1
@@ -37,4 +37,5 @@ $FF $VID $TONE44 -c:v libx264 -pix_fmt yuv420p -g 5 -bf 2 -c:a aac -b:a 48k -sho
|
||||
$FF $VID -c:v libx264 -pix_fmt yuv420p -g 5 -bf 0 $META clip-h264.mov
|
||||
$FF $VID -c:v libx265 -pix_fmt yuv420p -x265-params log-level=error:keyint=5 -tag:v hvc1 $META clip-hevc.mp4
|
||||
$FF $VID $TONE -c:v libvpx-vp9 -pix_fmt yuv420p -g 5 -b:v 100k -c:a libopus -b:a 32k -shortest $META clip-vp9.webm
|
||||
$FF $VID $TONE44 -c:v libvpx -pix_fmt yuv420p -g 5 -b:v 100k -deadline realtime -c:a vorbis -strict -2 -ac 2 -b:a 48k -shortest $META clip-vp8.webm
|
||||
$FF -f rawvideo -pix_fmt rgb24 -s 64x64 -r 10 -i "$TMP_DIR/deckcraft-fixture-64x64.rgb" -c:v libsvtav1 -pix_fmt yuv420p -g 5 -svtav1-params loglevel=0 $META clip-av1.mkv
|
||||
|
||||
@@ -736,6 +736,37 @@ mod tests {
|
||||
b
|
||||
}
|
||||
|
||||
/// A video whose codec has no decoder (a VP8 WebM, #1237) still plays through the real
|
||||
/// insert + play path: its audio track sounds, and the insert said the video won't show.
|
||||
#[test]
|
||||
fn a_webm_with_an_undecodable_video_codec_still_plays_its_audio() {
|
||||
use deckcraft_engine::Session;
|
||||
let bytes = include_bytes!("../../media/tests/fixtures/clip-vp8.webm");
|
||||
let mut s = Session::with_new();
|
||||
let _ = s.execute("slide.new", &serde_json::json!({"layout": "blank"}));
|
||||
let r = s
|
||||
.execute("insert.video", &serde_json::json!({"name": "clip.webm", "data": deckcraft_engine::cmd::base64_encode(bytes)}))
|
||||
.expect("insert");
|
||||
assert_eq!(r["playable"], true, "the Vorbis track plays: {r}");
|
||||
let w = r["warning"].as_str().expect("the insert says the video won't show");
|
||||
assert!(w.contains("VP8") && w.contains("only the audio plays"), "{w}");
|
||||
let (uid, doc) = s.active().map(|d| (d.uid, d.doc.clone())).expect("document");
|
||||
let id = ShapeId(r["id"].as_u64().expect("id") as u32);
|
||||
let clip = find_clip(&doc, id, Some(0)).expect("clip");
|
||||
let mut host = MediaHost::new(None);
|
||||
host.play(uid, &doc, id, &clip, None, Owner::Show);
|
||||
assert!(host.player.wait_loaded(key(id), std::time::Duration::from_secs(5)), "decode");
|
||||
assert!(host.status(id).is_some_and(|s| s.error.is_none()), "Vorbis decodes");
|
||||
let mut buf = vec![0.0f32; 22_050];
|
||||
host.player.render(&mut buf, 1, 44_100);
|
||||
// The fixture's sine is −21 dB, so a peak near 0.09 means it is sounding.
|
||||
let peak = buf.iter().fold(0.0f32, |m, s| m.max(s.abs()));
|
||||
assert!(peak > 0.04, "the audio track sounds: peak {peak}");
|
||||
assert!(host.videos.get(&id).is_some_and(|v| v.feed.is_none()), "VP8 opens no video feed");
|
||||
host.tick(&egui::Context::default(), &mut s);
|
||||
assert!(host.texture(id).is_none(), "no VP8 frame exists to show");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grouped_gifs_play_in_slide_space() {
|
||||
let mut doc = Presentation::default();
|
||||
|
||||
Reference in New Issue
Block a user