Skip to content

Commit ddaf92d

Browse files
committed
fix(export): stop drawing the screen inside the PiP box on camera-less clips
The layout preset is global — one panel for the whole timeline — but the camera is per clip: a project mixes a screen+webcam recording with a plain import without one. `LiveParams::has_webcam` already carried that distinction, but only `live.rs` derived it, so the preview was right and every export was wrong. An export sets its `LiveParams` once for the whole timeline (`compositor-view-napi`), keeping the `true` default. And `ExportDialog` sends the SCREEN path as `webcamPath` when a clip has no camera, purely so the decoder has something valid to open — so the PiP box was drawn with the screen recording behind it, duplicated into its own corner. That is the mirror reported in #248, which the greyed-out Layout panel (correctly gated on `hasAnyClipWithCamera`) then left no way to turn off. `webcam_is_real` moves next to the field it decides, and `walk_composited_timeline` — shared by MP4 and GIF on all three backends — rebinds it per clip. A targeted `set_has_webcam` rather than a per-clip `set_live_params`, which would clobber the settings the caller posted. Verified on a real export (`run_composited_multi`, h264_amf) with the camera path equal to the screen path: the thumbnail is gone. The bench gains a `--webcam` override because the no-camera case is not different *content* but an identical *path*, and cannot be replayed otherwise. Refs #248
1 parent a61d7a2 commit ddaf92d

7 files changed

Lines changed: 63 additions & 24 deletions

File tree

crates/compositor/src/compositor_linux.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,12 @@ impl Compositor {
726726
*self.live_params.borrow_mut() = p;
727727
}
728728

729+
/// Cf. `compositor_windows::set_has_webcam` — le seul champ de `LiveParams` qui dépend du
730+
/// clip courant, rebranché par `walk_composited_timeline` sans écraser le reste.
731+
pub fn set_has_webcam(&self, v: bool) {
732+
self.live_params.borrow_mut().has_webcam = v;
733+
}
734+
729735
pub fn set_scene(&self, s: Option<Scene>) {
730736
*self.scene.borrow_mut() = s;
731737
}

crates/compositor/src/compositor_macos.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,12 @@ impl Compositor {
577577
*self.live_params.borrow_mut() = p;
578578
}
579579

580+
/// Cf. `compositor_windows::set_has_webcam` — le seul champ de `LiveParams` qui dépend du
581+
/// clip courant, rebranché par `walk_composited_timeline` sans écraser le reste.
582+
pub fn set_has_webcam(&self, v: bool) {
583+
self.live_params.borrow_mut().has_webcam = v;
584+
}
585+
580586
pub fn set_scene(&self, s: Option<Scene>) {
581587
*self.scene.borrow_mut() = s;
582588
}

crates/compositor/src/compositor_windows.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,14 @@ impl Compositor {
596596
*self.live_params.borrow_mut() = p;
597597
}
598598

599+
/// Rebranche le seul champ qui dépend du CLIP et non des réglages (cf. `LiveParams::has_webcam`).
600+
/// L'export pose ses `LiveParams` une fois pour toute la timeline, mais chaque clip a sa propre
601+
/// réponse à « y a-t-il une caméra ? » : d'où un setter ciblé plutôt qu'un `set_live_params`
602+
/// par clip, qui écraserait les réglages posés par l'appelant.
603+
pub fn set_has_webcam(&self, v: bool) {
604+
self.live_params.borrow_mut().has_webcam = v;
605+
}
606+
599607
/// Installe (ou retire) la scène de l'app. Présente → `compose_frame` prend ses placements
600608
/// depuis le layout preset au lieu du planning fixture.
601609
pub fn set_scene(&self, s: Option<Scene>) {

crates/compositor/src/frame_geometry.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -601,12 +601,37 @@ pub struct LiveParams {
601601
/// False when the "webcam" decoder is actually just the screen video again (the TS side
602602
/// falls `webcamPath` back to the screen asset's own path when a clip has no real camera,
603603
/// purely so the decoder pipeline has something valid to open) — drawing the PiP box in
604-
/// that case duplicates the screen video into its own corner. Live-only: derived in
605-
/// `live.rs` by comparing the active clip's screen/webcam paths; defaults `true` (draw)
606-
/// so fixture/bench renders and any caller that never sets it keep their old behavior.
604+
/// that case duplicates the screen video into its own corner. Derived per clip from the
605+
/// screen/webcam paths via `webcam_is_real`: in `live.rs` for the preview, in
606+
/// `timeline_walk.rs` for every export. Defaults `true` (draw) so fixture/bench renders
607+
/// and any caller that never sets it keep their old behavior.
607608
pub has_webcam: bool,
608609
}
609610

611+
fn same_source_path(a: &str, b: &str) -> bool {
612+
a.eq_ignore_ascii_case(b)
613+
}
614+
615+
/// True when this clip really has a camera to draw.
616+
///
617+
/// TWO ways the app says "no camera", and both must be caught here, because the
618+
/// webcam decoder is opened either way — the live path falls back to the SCREEN
619+
/// file when the webcam path won't open, and `ExportDialog` sends the screen path
620+
/// outright, so the decoder always yields frames. Whether those frames are the
621+
/// camera or a second copy of the screen is decided HERE and nowhere else.
622+
///
623+
/// - the empty string, which is what `sceneDescription.ts` and
624+
/// `NativeCompositorOverlay` send for an asset with no `cameraTrack`;
625+
/// - the screen's own path, which `ExportDialog.tsx` sends and which older
626+
/// scenes still use.
627+
///
628+
/// Missing the empty-string case is what put the screen recording inside the PiP
629+
/// box: `"" != "/…/recording.mp4"`, so the box was drawn, and the decoder behind
630+
/// it was the screen fallback.
631+
pub fn webcam_is_real(webcam_path: &str, screen_path: &str) -> bool {
632+
!webcam_path.trim().is_empty() && !same_source_path(webcam_path, screen_path)
633+
}
634+
610635
impl Default for LiveParams {
611636
fn default() -> Self {
612637
Self {

crates/compositor/src/live.rs

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::scene::Scene;
2828
use crate::config::{self, Cfg};
2929
use crate::cursor::CursorTrack;
3030
use crate::d3d::Gpu;
31+
use crate::frame_geometry::webcam_is_real;
3132
use crate::pipeline::Decoder;
3233
use crate::timeline_walk::{frame_step, FrameStep, NextFrameTime};
3334
use anyhow::Result;
@@ -590,26 +591,6 @@ fn same_source_path(a: &str, b: &str) -> bool {
590591
a.eq_ignore_ascii_case(b)
591592
}
592593

593-
/// True when the active clip really has a camera to draw.
594-
///
595-
/// TWO ways the app says "no camera", and both must be caught here, because the
596-
/// webcam decoder is opened either way — `open_and_seek_clip` falls back to the
597-
/// SCREEN file when the webcam path won't open, so `wdec` always yields frames.
598-
/// Whether those frames are the camera or a second copy of the screen is decided
599-
/// HERE and nowhere else.
600-
///
601-
/// - the empty string, which is what `sceneDescription.ts` and
602-
/// `NativeCompositorOverlay` send for an asset with no `cameraTrack`;
603-
/// - the screen's own path, the older convention kept working for scenes that
604-
/// still use it.
605-
///
606-
/// Missing the empty-string case is what put the screen recording inside the PiP
607-
/// box: `"" != "/…/recording.mp4"`, so the box was drawn, and the decoder behind
608-
/// it was the screen fallback.
609-
fn webcam_is_real(webcam_path: &str, screen_path: &str) -> bool {
610-
!webcam_path.trim().is_empty() && !same_source_path(webcam_path, screen_path)
611-
}
612-
613594
fn scene_clip_matches(
614595
clip: &crate::scene::SceneClip,
615596
screen_path: &str,

crates/compositor/src/timeline_walk.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::compositor::Compositor;
1717
use crate::config::Cfg;
1818
use crate::cursor::CursorTrack;
1919
use crate::d3d::Gpu;
20+
use crate::frame_geometry::webcam_is_real;
2021
use crate::pipeline::{ClipSource, Decoder};
2122
use crate::regions::{speed_segments_for_window, SpeedSegment};
2223
use crate::scene::Scene;
@@ -164,6 +165,13 @@ pub(crate) unsafe fn walk_composited_timeline(
164165
let mut frames: u64 = 0;
165166

166167
for (clip_index, clip) in clips.iter().enumerate() {
168+
// Le preset de layout est GLOBAL (un seul panneau pour toute la timeline) mais la
169+
// caméra est PAR CLIP : un projet mélange sans problème un enregistrement avec webcam
170+
// et un import qui n'en a pas. Le preset ne doit donc s'appliquer qu'aux clips qui ont
171+
// vraiment une caméra — sinon la boîte PiP est dessinée avec, derrière, le décodeur de
172+
// repli, c'est-à-dire l'écran lui-même recopié dans son propre coin (issue #248).
173+
// La preview vive fait exactement ça dans `live.rs` ; c'est ici l'équivalent export.
174+
comp.set_has_webcam(webcam_is_real(&clip.webcam, &clip.screen));
167175
if !screen_decs.contains_key(&clip.screen) {
168176
screen_decs.insert(clip.screen.clone(), Decoder::open(&clip.screen, gpu)?);
169177
}

crates/poc-d3d/src/bench.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub fn run() -> Result<()> {
4848

4949
// poc-d3d.exe --cfg C0..C8 --fixture <dir> --repeat 3 --out out/
5050
// --cfg GIF → bench natif GIF (slice 1)
51+
// --webcam <path> → force le chemin caméra (défaut `<fixture>/webcam.mp4`)
5152
fn run_bench(args: &[String]) -> Result<()> {
5253
let get = |k: &str, d: &str| -> String { arg(args, k, d) };
5354
let fixture = get("--fixture", "fixture");
@@ -56,7 +57,11 @@ fn run_bench(args: &[String]) -> Result<()> {
5657
let cfg_arg = get("--cfg", "C0..C8");
5758

5859
let screen = format!("{fixture}/screen.mp4");
59-
let webcam = format!("{fixture}/webcam.mp4");
60+
// Override explicite parce que le cas « pas de caméra » n'est PAS un fichier
61+
// différent : l'app renvoie le chemin de l'écran lui-même (`ExportDialog`) ou la
62+
// chaîne vide (`sceneDescription`). Le reproduire demande donc de piloter le chemin,
63+
// pas le contenu — `--webcam <screen.mp4>` rejoue exactement l'issue #248.
64+
let webcam = get("--webcam", &format!("{fixture}/webcam.mp4"));
6065
std::fs::create_dir_all(&out).ok();
6166

6267
// sélection des cfg

0 commit comments

Comments
 (0)