diff --git a/README.md b/README.md index 59368c9..18ab281 100644 --- a/README.md +++ b/README.md @@ -287,9 +287,9 @@ machine's fonts, so it looks the same on every machine. An overlay's own images and fonts, such as a logo or a brand face, come from `data:` URLs or from files in the scene file's directory, named relative to it (`url(brand/logo.png)`), never from the web or anywhere else on disk. The -studio reads them all as the overlay starts, so its first frame has them, and -a file changed on disk changes nothing on air until the overlay starts again, -as when it is edited. Images are PNG or JPEG, at most 4096 pixels a side; +studio reads them all as the overlay opens, so its first frame has them, and +a file changed on disk changes nothing on air until the overlay opens again, +as when it is edited; a restart on program reuses what it read. Images are PNG or JPEG, at most 4096 pixels a side; fonts are TrueType, OpenType, WOFF, or WOFF2, loaded with `@font-face`: ```css @@ -318,8 +318,15 @@ element, attribute, rule, property, or asset it refused: draws more. An overlay's animation starts when the overlay does: when the studio opens -it, and again whenever it is edited. One that no preview or program shows -costs nothing until it is shown again, where its animation has carried on. +it, and again whenever it is edited. It also starts over each time a scene +showing it goes on program, as an item's keyframes do, so a lower third plays +its entrance at every cut, and when a scene showing it goes into preview while +program does not show it, so the preview shows the entrance too. Preview and +program share its timeline, so a preview never restarts one program shows. +Set `restart = false` (**Restart on program**, off, in the add form) for one +that runs on across cuts, such as a clock or a ticker. One that no preview or +program shows costs nothing until it is shown again, where its animation has +carried on unless it restarts. Overlays publish no track of their own, so viewers see them only in the program. @@ -512,6 +519,7 @@ height = 600 kind = "overlay" width = 1920 # the overlay's own size, like a page's height = 1080 +restart = true # start over each time it goes on program, the default html = '''
" + ), + width: 8, + height: 8, + restart, + }; + collection.sources.insert("third".into(), overlay(0, true)); + collection.sources.insert("clock".into(), overlay(4, false)); + let fill = Rect::fill(&collection.canvas); + collection.scenes[1].items = vec![Item::new("third", fill), Item::new("clock", fill)]; + let mut studio = Studio::new(collection).await; + let half = [128, 0, 128, 255]; + let near = + |actual: [u8; 4], expected: [u8; 4]| actual.iter().zip(expected).all(|(a, e)| a.abs_diff(e) <= 2); + + // Both open here, drawn nowhere. + let start = Instant::now(); + studio.compositor.set_preview(Some("red".into())); + studio.compositor.cut(&studio.sources); + studio.render(start); + + let previewed = start + Duration::from_secs(2); + studio.compositor.set_preview(Some("blue".into())); + studio.render(previewed); + studio.render(previewed + Duration::from_millis(500)); + let preview = studio.preview(); + assert!( + near(pixel(&preview, 3, 1), half), + "in preview: {:?}", + pixel(&preview, 3, 1) + ); + assert_eq!(pixel(&preview, 3, 6), BLUE, "the clock ran on"); + + let cut = previewed + Duration::from_secs(3); + studio.compositor.cut(&studio.sources); + studio.render(cut); + assert_eq!(pixel(&studio.program(), 3, 1), RED, "on program, from the start"); + studio.render(cut + Duration::from_millis(500)); + let program = studio.program(); + assert!( + near(pixel(&program, 3, 1), half), + "on program: {:?}", + pixel(&program, 3, 1) + ); + assert_eq!(pixel(&program, 3, 6), BLUE, "the clock ran on"); + + // Into preview again, but program shows it, so it runs on in both. + studio.compositor.set_preview(Some("blue".into())); + studio.render(cut + Duration::from_millis(750)); + let quarter = [64, 0, 191, 255]; + for (output, picture) in [("program", studio.program()), ("preview", studio.preview())] { + let actual = pixel(&picture, 3, 1); + assert!(near(actual, quarter), "{output}: {actual:?}"); + } + + // Nor does it restart one the top layer shows on program. + studio.collection.top = vec![Item::new("third", fill)]; + studio.compositor.set_preview(Some("red".into())); + studio.compositor.cut(&studio.sources); + studio.render(cut + Duration::from_secs(1)); + studio.compositor.set_preview(Some("blue".into())); + studio.render(cut + Duration::from_millis(1100)); + assert_eq!(pixel(&studio.program(), 3, 1), BLUE, "the top layer's ran on"); + }) + .await; + } + + /// A preview never restarts an overlay a stinger under way masks its clip + /// with on program, though no scene on air shows it. + #[tokio::test] + #[ignore = "needs a Vulkan device; run by just rs render-sw"] + async fn a_stinger_mask_runs_on_under_a_preview() { + local(async { + let mut studio = stinging(crate::media::tests::CLIP).await; + let overlay = Source::Overlay { + html: "".into(), + width: 8, + height: 8, + restart: true, + }; + studio.collection.sources.insert("third".into(), overlay); + let fill = Rect::fill(&studio.collection.canvas); + studio.collection.scenes.push(Scene { + items: vec![Item::new("third", fill)], + ..Scene::new("third") + }); + let mask = Filter::Mask { + source: "third".into(), + mode: MaskMode::Alpha, + }; + sting(&mut studio, mask); + let drivers = studio.sources.sync(&studio.collection); + start(&studio.sources, drivers).await; + + let now = Instant::now(); + let transition = studio.collection.transition.clone(); + studio.compositor.transition(transition, &studio.sources, now); + studio.render(now); + let opened = studio.compositor.layers["third"].overlay.as_ref().unwrap().start(); + + studio.compositor.set_preview(Some("third".into())); + studio.render(now + Duration::from_millis(100)); + assert_eq!(studio.compositor.stinger(), Some("sting")); + let overlay = studio.compositor.layers["third"].overlay.as_ref().unwrap(); + assert_eq!(overlay.start(), opened, "the stinger's mask ran on"); + }) + .await; + } + /// An overlay a move glides is painted on the program clock while both /// scenes are on air, and drawn where the move has it. #[tokio::test] @@ -3162,6 +3360,7 @@ mod tests { html: "".into(), width: 4, height: 4, + restart: true, }, ); let tile = |x, y| Rect { @@ -3205,6 +3404,7 @@ mod tests { html: format!(""), width: 8, height: 8, + restart: true, }; collection.sources.insert("shown".into(), overlay("lime")); collection.sources.insert("aside".into(), overlay("yellow")); diff --git a/rs/moq-scene/src/overlay.rs b/rs/moq-scene/src/overlay.rs index 782427d..15e32c0 100644 --- a/rs/moq-scene/src/overlay.rs +++ b/rs/moq-scene/src/overlay.rs @@ -924,6 +924,8 @@ enum Request { start: Instant, }, Close(u64), + /// Start an overlay's CSS timeline over at this time on the program clock. + Restart(u64, Instant), /// Paint these overlays as of this time on the program clock. Paint(Instant, Vec