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: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Every panel is a tab you can drag, split, or stack.
| Devices | List this machine's cameras, displays, windows, and audio inputs, and add one as a source. |
| Guests | Invite a guest, show their join link as a QR code, see who joined, and take a guest out of every scene. |
| Filters | Crop the selected source or item, add, order, and set its filters, set an item's blend mode, and copy or paste filters. |
| Keyframes | Animate the selected item: add, time, ease, and remove its keyframes, and set how far each moves, resizes, crops, and filters it (see [Keyframes](#keyframes)). **Play** plays the preview scene's keyframes again. |
| Audio | Add, rename (double-click), and remove mixes, pick the one on program and the one to listen to and on which output, and set each audio source's gain, mute, delay, and the mixes it feeds, with a meter on every mix and source. |
| Output | Start and stop recording, pick the mix it records, set the canvas size and frame rate, and see the broadcast path, whether the program is encoding, the encoder sessions open, and each rendition's encoder work. |
| Health | See how the stream reaches viewers, relay by relay and track by track, and the cause of each problem. |
Expand All @@ -122,6 +123,29 @@ the stream and a recording show the same frames of it, and a second
**Transition** while one runs starts from the scene it was heading to. A
transition set to 0 ms is a cut.

### Keyframes

An item's keyframes animate it: an item can fly in, fade in, or blur into
focus, and a camera can push in. Each keyframe is a time after the item's
scene goes on and how far the item is from where it rests then: moved and
resized by canvas pixels, cropped further, and drawn through filters after its
own. Between two keyframes the item eases from one to the next, linearly or
easing in, out, or (by default) both. Filters of the same kinds, in order,
mix their settings, and others change halfway. A keyframe's filters cannot be
a LUT or a mask.

Keyframes play once, from when the item's scene goes on program, and hold the
last keyframe; before the first, the item holds the first. A cut or transition
starts them again, even to the scene already on program, while a scene leaving
in a transition carries on with its own. The preview plays them from when the
scene went into preview; pick the scene again, or press **Play**, to watch them
again. They are drawn on the program clock, so the stream, a recording, and a
replay of the program show the same frames. Top-layer items have none, since
the top layer never goes on.

Dragging an item in the preview grabs it where its keyframes have it and moves
where it rests, so its whole animation moves with it.

In the preview, click an item of whatever the Items panel shows to select it,
drag it to move it, and drag its edges or corners to resize it. A drag snaps
edges and centers to the canvas's edges and center, to the other items shown,
Expand Down Expand Up @@ -429,6 +453,11 @@ items = [
name = "Break"
items = [
{ source = "camera-faded", rect = { x = 0, y = 0, width = 1920, height = 1080 } },
# Flies in from the right, blurred, over half a second, landing where it rests.
{ source = "guest", rect = { x = 1280, y = 720, width = 640, height = 360 }, keyframes = [
{ at = "0s", x = 700, filters = [{ kind = "blur", radius = 12 }] },
{ at = "500ms", ease = "out", filters = [{ kind = "blur", radius = 0 }] },
] },
]

[[top]]
Expand Down Expand Up @@ -477,6 +506,12 @@ An item's `blend` mixes it with what is under it by a CSS `mix-blend-mode`:
`saturation`, `color`, or `luminosity`. Removing a source removes the filters
that name it.

An item's `keyframes`, in order of time, each have an `at`, how long after the
scene goes on, and optionally an `ease` into it from the one before (`linear`,
`in`, `out`, or `in_out`, the default), `x`, `y`, `width`, and `height` added
to the item's `rect`, a `crop` of what the item's crop leaves, and `filters`
applied after the item's own.

The studio writes the file a second after the last change, so a burst such as a
slider drag is one write, and once more on quit. Edits that never pause for a
second are written five seconds after the first, so a crash loses at most the
Expand Down
1 change: 0 additions & 1 deletion quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ repository.
- [Studio on macOS with capture granted](/quest/m0/app-macos.md) - camera, screen, and audio capture run on a Mac, not only Linux
- [Studio on Windows](/quest/m0/app-windows.md) - the studio runs on Windows, not only Linux
- [Wayland display and window sources](/quest/m0/wayland-sources.md) - portal-picked sources that reopen without the picker
- [Keyframes](/quest/m0/animation.md) - keyframed item properties with easing, starting when their scene goes on program
- [Media file source](/quest/m0/media-file.md) - a local video or audio file plays as a source, looping or from the start on demand
- [Stinger transitions](/quest/m0/stinger.md) - a clip played over the program that switches scenes at its cut point
- [ML video effects](/quest/m0/effects.md) - background removal and other ML effects, baked into their source's track
Expand Down
35 changes: 0 additions & 35 deletions quest/m0/animation.md

This file was deleted.

2 changes: 1 addition & 1 deletion quest/m0/stinger.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ program that switches scenes at its cut point, as broadcast graphics do.

## Plan

Split from [Keyframes](/quest/m0/animation.md) on 2026-11-04 (maintainer). A
Split from the keyframes quest on 2026-11-04 (maintainer). A
stinger is a transition kind beside fade, slide, wipe, and move
([moq-dev/studio#33](https://github.com/moq-dev/studio/pull/33)), drawn on the
program clock, whose clip is a media file source. It must start its clip as
Expand Down
4 changes: 4 additions & 0 deletions quest/m1/compose/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ themselves; off, viewers see the page only in the program. A replay source
layout says when it is hidden and when its span has ended, rather than leaving
a viewer on its last frame.

Items carry their keyframes (`moq_scene::Keyframe`), which play from when
their scene went on program, so the layout says when that was on the media
clock, and a viewer that joins late lands at the same point of them.

Only sources the creator publishes independently
([Published independently](/quest/m0/publish-independently.md)) have tracks a
viewer can fetch; the rest reach viewers only in the program.
34 changes: 31 additions & 3 deletions rs/moq-scene/src/clip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use std::path::Path;
use serde::{Deserialize, Serialize};

use crate::Error;
use crate::collection::{Audio, Collection, Filter, Item, Layer, Mixes, Source, Video, validate_filters, versioned};
use crate::collection::{
Audio, Collection, Filter, Item, Layer, Mixes, Scene, Source, Video, validate_filters, versioned,
};
use crate::edit::Draft;

/// What a copy holds: items, or a filter chain.
Expand Down Expand Up @@ -114,8 +116,12 @@ impl Clip {
},
audio: items.audio.clone(),
video: items.video.clone(),
scenes: Vec::new(),
top: items.items.clone(),
// In a scene, where keyframes play; pasting into the top
// layer checks it holds none.
scenes: vec![Scene {
items: items.items.clone(),
..Scene::new("clip")
}],
..Collection::new()
}
.validate()?
Expand Down Expand Up @@ -294,6 +300,28 @@ mod tests {
assert_eq!(pasted.top, collection.scenes[0].items);
}

/// An animated item copies with its keyframes into a scene, and the top
/// layer, which never goes on to play them, refuses it.
#[test]
fn pastes_keyframes_into_a_scene_only() {
let mut collection = collection();
collection.scenes[0].items[0].keyframes = vec![crate::Keyframe {
x: -100.0,
..crate::Keyframe::new(std::time::Duration::ZERO)
}];
let clip = Clip::items(&collection, collection.scenes[0].items.clone(), Path::new(DIR));
let text = clip.to_toml().unwrap();
let pasted = paste(&collection, &Layer::Scene("Scene".into()), &text, DIR);
assert_eq!(pasted.scenes[0].items[1], collection.scenes[0].items[0]);

let Clip::Items(items) = Clip::from_toml(&text).unwrap() else {
panic!("not items")
};
let mut draft = collection.draft("Paste");
draft.paste(&Layer::Top, &items, Path::new(DIR)).unwrap();
assert!(matches!(draft.finish(), Err(Error::Invalid(_))));
}

/// Into another collection, a clip brings what its items show, renaming a
/// source whose name is taken by another, and its clone follows the rename.
#[test]
Expand Down
Loading