Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ Each further canvas publishes a broadcast of its own beneath the main one, at
encoding it again. Removing a canvas ends its broadcast, and removes its top
layer and every scene's layout on it.

### Scene sources

A **scene source** draws another scene as one picture, which an item places,
scales, crops, filters, and blends like any other: a lower third built once
and shown in every scene, or a whole layout shrunk into a corner. Add one in
the Sources panel as kind **Scene**, picking the scene it draws, or
**Program**, which shows whatever is on air, a transition under way included,
on the same frame, such as for a portrait canvas that follows the program,
cropped.

A nested scene is drawn at the main canvas's size, transparent where it draws
nothing, once per frame however many items show it, and only while something
drawn shows it. Its items' keyframes play from when the scene showing it goes
on, as that scene's own items' do, so the top layer, which never goes on,
cannot show a scene with keyframes. A media file or overlay in a nested scene
set to restart starts over when the scene showing it goes on program.

A scene cannot draw itself, even through other scenes, so an edit that would
is refused. A scene showing the program cannot go on program, nor can the main
canvas's top layer show it, since either would draw the program into itself:
**Cut** and **Transition** refuse such a scene, and an edit making the scene on
air show the program is refused. Renaming a scene renames what its scene
sources draw, and removing one removes them, with the items showing them.

### Transitions

| Transition | What it does |
Expand Down Expand Up @@ -543,6 +567,13 @@ delay = "80ms"
kind = "clone"
source = "camera"

[sources.main-view]
kind = "scene"
scene = "Main" # draws the Main scene as one picture

[sources.on-air]
kind = "program" # whatever the program shows

[video.camera-faded]
filters = [{ kind = "opacity", amount = 0.5 }]

Expand Down Expand Up @@ -579,6 +610,7 @@ items = [
{ at = "0s", x = 700, filters = [{ kind = "blur", radius = 12 }] },
{ at = "500ms", ease = "out", filters = [{ kind = "blur", radius = 0 }] },
] },
{ source = "main-view", rect = { x = 80, y = 80, width = 640, height = 360 } },
]

[[top]]
Expand All @@ -593,7 +625,8 @@ Source kinds are `camera`, `display`, and `microphone` (each with an optional
`broadcast`, `browser` (a `url`, and a `width` and `height` up to 4096),
`overlay` (its `html`, a `width` and `height` up to 4096, and whether it `restart`s, true by default), `clone` (with the `source` it shows), `lut` (a 3D Adobe or Resolve `.cube`
file, which no item places, but which grades what a `lut` filter names it in),
and `replay`, where the Replay panel plays replays. The Devices panel fills in
`replay`, where the Replay panel plays replays, `scene` (with the `scene` it
draws), and `program` (see [Scene sources](#scene-sources)). The Devices panel fills in
ids. `[[top]]` items make up the top layer.

`[canvases.<name>]` adds a further canvas, with its `width` and `height` and
Expand Down
1 change: 0 additions & 1 deletion quest/m0/scenes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,5 @@ reads skips drawing. Per-scene cost grows only with what that scene draws;

## Required

- [Scene source](/quest/m0/scenes/scene-source.md) - a scene draws another scene, or the live program, as a source
- [Published scenes](/quest/m0/scenes/published.md) - any scene publishes as its own broadcast at its own size, replacing canvases
- [Audio scenes](/quest/m0/scenes/audio.md) - mixes become audio-only scenes that nest
4 changes: 0 additions & 4 deletions quest/m0/scenes/audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,3 @@ Decided 2026-11-05 with the maintainer (/quest-plan); see the
- Switching or editing an audio scene never drops or repeats audio, as with
mixes today.
- `README.md` documents audio scenes.

## Required

- [Scene source](/quest/m0/scenes/scene-source.md) - shares the scene graph and its cycle check
9 changes: 3 additions & 6 deletions quest/m0/scenes/published.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ broadcast at `<--broadcast>/<scene>`, at its own size, with the audio scene it
picks (the one on program by default). This replaces canvases: further
canvases, their per-scene layouts, and the canvas picker are removed, and a
portrait broadcast is a published portrait scene that draws the program
through a [scene source](/quest/m0/scenes/scene-source.md).
through a program source (`kind = "program"`).

## Plan

Decided 2026-11-05 with the maintainer (/quest-plan); see the
[epic](/quest/m0/scenes/README.md).

- Every scene has a size, defaulting to the program's; the switcher offers
only scenes of the program's size.
only scenes of the program's size. A scene source then draws its scene at
that size, rather than the main canvas's as it does now.
- The program keeps one switcher at `<--broadcast>`. A published scene is
always live and never switched.
- Each published scene gets the program's two sizes (full and half) and the
Expand All @@ -26,7 +27,3 @@ Decided 2026-11-05 with the maintainer (/quest-plan); see the
and no output reads skips drawing.
- Remove `[canvases]` from the scene file with no migration; nothing is
released. `README.md` documents published scenes.

## Required

- [Scene source](/quest/m0/scenes/scene-source.md) - a portrait scene follows the program through it
23 changes: 0 additions & 23 deletions quest/m0/scenes/scene-source.md

This file was deleted.

58 changes: 54 additions & 4 deletions rs/moq-scene/src/clip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,26 @@ impl Clip {
match &clip {
// The rules a collection holding just the clip would break, such as
// an item showing a source the clip lacks. It has whatever mixes the
// clip's sources are left out of, which pasting checks.
// clip's sources are left out of, and an empty scene for each its
// scene sources draw, which pasting checks.
Self::Items(items) => {
let excluded = items.audio.values().flat_map(|audio| &audio.exclude);
let mut names: BTreeSet<String> = excluded.cloned().collect();
names.insert(Mixes::DEFAULT.into());
let drawn: BTreeSet<&str> = items
.sources
.values()
.filter_map(|source| match source {
Source::Scene { scene } => Some(scene.as_str()),
_ => None,
})
.collect();
// Named apart from them, so it draws none of them by name.
let clip = std::iter::once("clip".to_string())
.chain((2..).map(|n| format!("clip {n}")))
.find(|name| !drawn.contains(name.as_str()))
.expect("an unused name");
let drawn = drawn.into_iter().map(Scene::new);
Collection {
sources: items.sources.clone(),
mixes: Mixes {
Expand All @@ -118,10 +133,12 @@ impl Clip {
video: items.video.clone(),
// In a scene, where keyframes play; pasting into the top
// layer checks it holds none.
scenes: vec![Scene {
scenes: std::iter::once(Scene {
items: items.items.clone(),
..Scene::new("clip")
}],
..Scene::new(clip)
})
.chain(drawn)
.collect(),
..Collection::new()
}
.validate()?
Expand Down Expand Up @@ -290,6 +307,39 @@ mod tests {
pasted
}

/// An item showing a scene pastes linked within its collection, but not
/// into one without that scene, nor into the scene it shows.
#[test]
fn a_scene_source_pastes_where_its_scene_is() {
let mut collection = collection();
let fill = Rect::fill(&collection.canvas);
collection.scenes.push(Scene::new("clip"));
collection.scenes.push(Scene::new("Other"));
let shown = Source::Scene { scene: "clip".into() };
collection.sources.insert("nested".into(), shown);
collection.scenes[1].items.push(Item::new("cam", fill));
collection.scenes[2].items.push(Item::new("nested", fill));
collection.validate().unwrap();
let clip = Clip::items(&collection, collection.scenes[2].items.clone(), Path::new(DIR));
let text = clip.to_toml().unwrap();

let pasted = paste(&collection, &Layer::scene("Scene"), &text, DIR);
assert_eq!(pasted.sources, collection.sources, "linked");

let Clip::Items(items) = Clip::from_toml(&text).unwrap() else {
panic!("not items")
};
let refused = |collection: &Collection, layer: &Layer| {
let mut draft = collection.draft("Paste");
draft.paste(layer, &items, Path::new(DIR)).unwrap();
draft.finish().unwrap_err().to_string()
};
let err = refused(&collection, &Layer::scene("clip"));
assert!(err.contains("scene `clip` draws itself"), "{err}");
let err = refused(&Collection::new(), &Layer::scene("Scene"));
assert!(err.contains("draws scene `clip`, which is not a scene"), "{err}");
}

/// Within a collection, a pasted item shows the very source copied.
#[test]
fn pastes_linked_within_a_collection() {
Expand Down
Loading