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
81 changes: 69 additions & 12 deletions crates/compositor/src/live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ fn webcam_seek_time(screen_source_time_sec: f64, webcam_offset_sec: f64) -> f64
(screen_source_time_sec - webcam_offset_sec).max(0.0)
}

/// The playback accumulator that puts the clock at `t` on a frame just positioned there. The
/// clock is the frame's pts plus `acc`, and a held frame's pts can be seconds before `t`.
fn anchor_acc(t: f64, pts: f64) -> f64 {
(t - pts).max(0.0)
}

/// Décodeurs déjà ouverts ET positionnés au bon playhead pour un clip à venir — le résultat
/// d'un préchargement en tâche de fond (voir `open_and_seek_clip`/`maybe_start_prefetch`
/// dans `render_thread`). Appliquer ceci à un `Player` (`apply_prefetched`) ne fait plus
Expand Down Expand Up @@ -391,6 +397,8 @@ impl Player {
self.idx = (source_time_sec * self.sdec.fps()).round().max(0.0) as u32;
self.has_current_frame = true;
self.use_current_on_next_step = true;
// Not composed yet: a `recompose` before the next frame must not reuse the old time.
self.composed_time_sec.set(self.sdec.cur_time_sec());
Ok(true)
}

Expand Down Expand Up @@ -424,6 +432,8 @@ impl Player {
self.idx = incoming.idx;
self.has_current_frame = true;
self.use_current_on_next_step = true;
// Not composed yet: a `recompose` before the next frame must not reuse the old clip's time.
self.composed_time_sec.set(self.sdec.cur_time_sec());
outgoing
}

Expand Down Expand Up @@ -592,7 +602,13 @@ impl Player {
}

self.has_current_frame = true;
self.sync_time(comp);
if use_current {
// A frame just positioned by a seek or a clip switch can be a held one, seconds
// before the playhead: compose it at the clock, which the caller anchored there.
self.sync_time_at(comp, target_source_time.max(self.sdec.cur_time_sec()));
} else {
self.sync_time(comp);
}
comp.compose_frame(sf, wf, self.idx as f32, cfg)?;
self.idx = self.idx.wrapping_add(1);
Ok(true)
Expand Down Expand Up @@ -670,8 +686,20 @@ impl Player {
}

/// Recompose la frame courante (déjà décodée) — rafraîchit après un changement de param.
/// At the time it was last composed at, not at its pts: a held frame placed by a seek on
/// Linux, or held when playback paused, is shown at the playhead, and a settle pass or a
/// param change sent the cursor and the published position back to the pts.
pub unsafe fn recompose(&self, comp: &Compositor, cfg: &Cfg) -> Result<bool> {
self.recompose_at(comp, cfg, self.sdec.cur_time_sec())
self.recompose_at(comp, cfg, self.composed_time_sec.get())
}

/// Anchors the playhead at `t` on a frame positioned but not composed yet (a re-anchor or
/// a clip change from the app): a `recompose` composes it there, and the returned
/// accumulator puts the playback clock there (`anchor_acc`).
pub unsafe fn anchor_at(&self, t: f64) -> f64 {
let pts = self.sdec.cur_time_sec();
self.composed_time_sec.set(t.max(pts));
anchor_acc(t, pts)
}

/// Recomposes the current frame with the cursor, zooms and programme time at `clock_sec`
Expand Down Expand Up @@ -715,7 +743,9 @@ impl Player {
}
self.has_current_frame = true;
self.use_current_on_next_step = false;
self.sync_time(comp);
// At `target_sec`, not at the held frame's pts: the cursor and zooms show where the
// playhead is, and the published position matches the app's.
self.sync_time_at(comp, target_sec.max(self.sdec.cur_time_sec()));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// "idx" ne sert plus qu'au fallback fixture (jamais lu si une scène est posée) — dérivé
// du temps réel pour rester cohérent si jamais consulté.
self.idx = (target_sec * self.sdec.fps()).round().max(0.0) as u32;
Expand Down Expand Up @@ -1418,6 +1448,9 @@ unsafe fn maybe_start_prefetch(
/// bascule est instantanée (juste un échange de champs, `Player::apply_prefetched`) ; sinon
/// on retombe sur l'ouverture synchrone habituelle (`Player::set_active_clip`) — correct dans
/// tous les cas, juste plus lent quand le préchargement n'a pas eu le temps de finir.
///
/// Returns the source time the next clip starts at when the switch happened, for the caller to
/// anchor its clock there (`anchor_acc`): the clip can start inside a still stretch.
#[allow(clippy::too_many_arguments)]
unsafe fn advance_to_next_scene_clip(
player: &mut Player,
Expand All @@ -1431,9 +1464,9 @@ unsafe fn advance_to_next_scene_clip(
raw_cursor: &mut Option<CursorTrack>,
loaded_cursor_path: &mut String,
last_smoothing: &mut f32,
) {
) -> Option<f64> {
let Some(next_index) = next_clip_index(scene, *active_clip_index) else {
return;
return None;
};
let next_clip = &scene.clips[next_index];

Expand Down Expand Up @@ -1505,8 +1538,12 @@ unsafe fn advance_to_next_scene_clip(
None => comp.clear_cursor(),
}
*last_smoothing = -1.0;
Some(next_clip.source_start_sec)
}
Err(e) => {
eprintln!("[live] auto-advance clip: {e:#}");
None
}
Err(e) => eprintln!("[live] auto-advance clip: {e:#}"),
}
}

Expand Down Expand Up @@ -1666,6 +1703,9 @@ unsafe fn render_thread(
};
match switch_result {
Ok(()) => {
// The app's re-anchor names the playhead: the clock restarts there, and a
// paused recompose (the `clip_changed` branch below) composes there.
acc = player.anchor_at(request.source_time_sec);
// Condition sur `repositioned`, pas sur `same_media` : un repositionnement
// qui a échoué est retombé sur l'ouverture complète, donc des décodeurs
// ONT été fermés et le cache doit être vidé malgré des médias identiques.
Expand Down Expand Up @@ -1861,7 +1901,9 @@ unsafe fn render_thread(
// affiché jusqu'à ce qu'une autre action provoque un compose.
(settle_until, last_settle) = open_settle_window(now);
}
acc = 0.0; // resynchronise l'accumulateur de lecture libre après un seek
// Resynchronise l'accumulateur de lecture libre après un seek : l'horloge repart du
// temps demandé, pas du pts de la frame tenue (`anchor_acc`).
acc = anchor_acc(target, player.screen_time_sec());
} else if shared.playing.load(Ordering::Relaxed) {
// BUG corrigé : la lecture libre décodait auparavant exactement 1 frame RÉELLE par
// tranche de 1/60s de temps réel écoulé, quelle que soit la densité effective de
Expand Down Expand Up @@ -1928,7 +1970,7 @@ unsafe fn render_thread(
break;
}
if player.screen_time_sec() >= clip.source_end_sec {
advance_to_next_scene_clip(
if let Some(start) = advance_to_next_scene_clip(
&mut player,
&comp,
scene,
Expand All @@ -1940,7 +1982,9 @@ unsafe fn render_thread(
&mut raw_cursor,
&mut loaded_cursor_path,
&mut last_smoothing,
);
) {
acc = anchor_acc(start, player.screen_time_sec());
}
}
}
}
Expand All @@ -1954,9 +1998,10 @@ unsafe fn render_thread(
// finit alors par boucler tout seul sur l'EOF réel (temps qui recule
// brutalement). On détecte ce recul et on corrige immédiatement en enchaînant
// sur le clip suivant, plutôt que de rester bloqué sur le 1er clip.
let mut crossed_anchor = None;
if let (Some(scene), Some(t_before)) = (&full_scene, screen_time_before_step) {
if player.screen_time_sec() < t_before {
advance_to_next_scene_clip(
crossed_anchor = advance_to_next_scene_clip(
&mut player,
&comp,
scene,
Expand All @@ -1968,17 +2013,22 @@ unsafe fn render_thread(
&mut raw_cursor,
&mut loaded_cursor_path,
&mut last_smoothing,
);
)
.map(|start| anchor_acc(start, player.screen_time_sec()));
}
}
if let Some(anchored) = crossed_anchor {
acc = anchored;
}
if !committed {
// Rien n'est dû pour l'instant (hold) : `acc` reste tel quel — il continue
// de s'accumuler aux ticks suivants jusqu'à ce qu'une vraie frame arrive.
break;
}
stepped = true;
let after = player.screen_time_sec();
acc = consume_acc(acc, before, after);
// A switch just anchored the clock on the new clip: nothing of the old one to consume.
acc = crossed_anchor.unwrap_or_else(|| consume_acc(acc, before, after));
n += 1;
if n >= max_steps {
// Rattrapage plafonné : le contenu dû est plus dense que ce qu'on peut décoder
Expand Down Expand Up @@ -2408,6 +2458,13 @@ mod tests {

use super::*;

#[test]
fn the_clock_restarts_at_the_requested_time_on_a_held_frame() {
// Before: the clock restarted at the held frame's pts, 0.9 s behind the playhead.
assert_eq!(hold_clock_sec(0.1, anchor_acc(1.0, 0.1), Some(5.0)), 1.0);
assert_eq!(anchor_acc(1.0, 1.02), 0.0, "never behind the frame's own pts");
}

fn multiclip_scene() -> Scene {
Scene::from_json(r##"{
"clips": [
Expand Down
14 changes: 12 additions & 2 deletions crates/compositor/tests/held_frame_clock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ fn a_held_frame_follows_the_playback_clock() {
assert!(player.present_frame(&comp, &cfg, 1.0).expect("seek"), "paused seek");
let held = player.screen_time_sec();
let at_pts = player.programme_time().expect("programme clock");
assert_eq!(player.composed_time_sec(), 1.0f64.max(held), "a seek reports the playhead");

comp.set_programme_time(None);
assert!(player.recompose_at(&comp, &cfg, held + 0.25).expect("recompose_at"));
Expand All @@ -66,10 +67,19 @@ fn a_held_frame_follows_the_playback_clock() {
"programme time {advanced} instead of {at_pts} + 0.25 s"
);

// Paused there, a param change or a settle pass recomposes: the frame keeps the time it
// was shown at. At the pts, the cursor and the published position went back.
comp.set_programme_time(None);
assert!(player.recompose(&comp, &cfg).expect("recompose"));
assert_eq!(comp.programme_time(), Some(at_pts), "recompose stays at the pts");
assert_eq!(player.composed_time_sec(), held);
assert_eq!(comp.programme_time(), Some(advanced), "recompose keeps the time it was composed at");
assert_eq!(player.composed_time_sec(), held + 0.25);

// A re-anchor positions the decoders without composing: the next recompose is at the
// anchored playhead, never at the time the frame before it was composed at.
assert!(player.seek_active(0.5).expect("seek_active"), "re-anchor");
player.anchor_at(0.5);
assert!(player.recompose(&comp, &cfg).expect("recompose"));
assert_eq!(player.composed_time_sec(), 0.5f64.max(player.screen_time_sec()));
}
}

Expand Down
Loading