mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-28 20:47:15 -06:00
Media: a video with an unsupported codec says only its audio plays (#1237)
insert.video answered playable=true with no warning for a clip whose video codec has no decoder (a VP8 + Vorbis WebM, say): its audio track plays, but the slide shows an unexplained black box, which reads as "black with no audio". The insert now warns that the video can't be played yet and only the audio plays. The missing audio itself is not reproducible: through the real insert + play path the Vorbis track of a VP8 WebM decodes and sounds, on main and on 0.10.0 (the reporter's build). A regression test drives that whole path, on a new synthesised clip-vp8.webm fixture. Co-Authored-By: GLM-5.3 <noreply@z.ai>
This commit is contained in:
co-authored by
GLM-5.3
parent
9641817a15
commit
238b34be00
+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 |
|
||||
|
||||
@@ -1278,14 +1278,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(|| {
|
||||
|
||||
@@ -286,4 +286,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
|
||||
|
||||
@@ -727,6 +727,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