Skip to content

Commit aeff270

Browse files
Merge branch 'main' into codex/carry-rounded-timecodes
2 parents 193a728 + 030ccf1 commit aeff270

75 files changed

Lines changed: 3034 additions & 526 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,3 +507,64 @@ jobs:
507507
--latest \
508508
--title "$TAG"
509509
fi
510+
511+
- name: Refresh the docs /download page
512+
# Only a stable release changes what /releases/latest resolves to, so a
513+
# pre-release would rebuild the site to byte-identical output.
514+
#
515+
# Dispatched against main on purpose: the github-pages environment only
516+
# permits `main` to deploy, so docs.yml's old `on: release` trigger ran
517+
# with a tag ref and failed its deploy every time. See docs.yml.
518+
if: ${{ steps.release.outputs.is_prerelease == 'false' }}
519+
timeout-minutes: 20
520+
env:
521+
GH_TOKEN: ${{ secrets.OPENSCREEN_RELEASE_TOKEN }}
522+
run: |
523+
latest_dispatch() {
524+
gh run list \
525+
--repo "$GITHUB_REPOSITORY" \
526+
--workflow docs.yml \
527+
--event workflow_dispatch \
528+
--branch main \
529+
--limit 1 \
530+
--json databaseId \
531+
--jq '.[0].databaseId // empty'
532+
}
533+
534+
# `gh workflow run` prints nothing we can key off, so remember which
535+
# dispatch was newest beforehand and wait for a different one to appear.
536+
PREVIOUS_RUN_ID="$(latest_dispatch)"
537+
gh workflow run docs.yml --ref main --repo "$GITHUB_REPOSITORY"
538+
539+
RUN_ID=""
540+
for _ in $(seq 1 30); do
541+
sleep 5
542+
CANDIDATE="$(latest_dispatch)"
543+
if [[ -n "$CANDIDATE" && "$CANDIDATE" != "$PREVIOUS_RUN_ID" ]]; then
544+
RUN_ID="$CANDIDATE"
545+
break
546+
fi
547+
done
548+
549+
if [[ -z "$RUN_ID" ]]; then
550+
echo "::error::Dispatched docs.yml but no new run appeared within 150s"
551+
exit 1
552+
fi
553+
554+
gh run watch "$RUN_ID" --repo "$GITHUB_REPOSITORY" --interval 15 || true
555+
556+
CONCLUSION="$(gh run view "$RUN_ID" --repo "$GITHUB_REPOSITORY" --json conclusion --jq '.conclusion')"
557+
case "$CONCLUSION" in
558+
success)
559+
echo "Docs rebuilt and deployed by run $RUN_ID"
560+
;;
561+
cancelled)
562+
# docs.yml cancels in-flight runs sharing a ref, so a push to main
563+
# landing right now replaces this rebuild with a newer one.
564+
echo "::warning::Docs run $RUN_ID was cancelled, most likely superseded by a newer main run"
565+
;;
566+
*)
567+
echo "::error::Docs run $RUN_ID concluded '$CONCLUSION' - /download may still list the previous release"
568+
exit 1
569+
;;
570+
esac

.github/workflows/docs.yml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ on:
1212
- "website/**"
1313
- ".github/workflows/docs.yml"
1414
# The /download page resolves the current release's assets at build time so it
15-
# can link each platform to its actual file. Without this trigger that data
16-
# would freeze at whatever the last website/** change saw, and the page would
17-
# keep serving the previous version's binaries after every release.
18-
# Pre-releases are skipped: /releases/latest ignores them, so the built output
19-
# would be byte-identical.
20-
release:
21-
types: [published]
15+
# can link each platform to its actual file. Without a post-release rebuild that
16+
# data would freeze at whatever the last website/** change saw, and the page
17+
# would keep serving the previous version's binaries after every release.
18+
#
19+
# That rebuild is a `workflow_dispatch` fired by build.yml once the release is
20+
# published, NOT an `on: release` trigger. A release event runs with
21+
# github.ref = refs/tags/vX.Y.Z, and the github-pages environment only allows
22+
# `main` to deploy, so the deploy job failed on every stable release (it never
23+
# surfaced earlier because pre-releases skipped the build entirely). Dispatching
24+
# against main both satisfies that policy and publishes main's docs rather than
25+
# the release branch's older snapshot.
2226
workflow_dispatch:
2327

2428
# Cancel in-flight runs on the same ref so fast follow-up pushes
@@ -34,9 +38,6 @@ jobs:
3438
build:
3539
name: Build site
3640
runs-on: ubuntu-latest
37-
# A pre-release does not change what /releases/latest resolves to, so
38-
# rebuilding for one would burn a run to produce identical output.
39-
if: github.event_name != 'release' || github.event.release.prerelease == false
4041
steps:
4142
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
4243
with:
@@ -66,7 +67,7 @@ jobs:
6667
needs: build
6768
if: >-
6869
(github.event_name == 'push' && github.ref == 'refs/heads/main')
69-
|| github.event_name == 'release'
70+
|| (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
7071
environment:
7172
name: github-pages
7273
url: ${{ steps.deployment.outputs.page_url }}

crates/compositor/src/linux_decode.rs

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use anyhow::{bail, Context, Result};
1616
use std::ffi::CString;
1717
use std::ptr;
1818

19+
use crate::timeline_walk::NextFrameTime;
20+
1921
use crate::ffi::{
2022
av_frame_alloc, av_frame_free, av_frame_move_ref, av_frame_unref, av_packet_alloc,
2123
av_packet_free, av_packet_unref, av_read_frame, av_seek_frame, avcodec_alloc_context3,
@@ -58,6 +60,11 @@ pub struct SwDecoder {
5860
frame: *mut AVFrame,
5961
sent_eof: bool,
6062
cur_pts: Option<i64>,
63+
/// Buffer de lookahead pour `peek_next_time_sec` : symétrique de
64+
/// `pipeline_macos::Decoder::peek_frame` — cf. là-bas pour la justification.
65+
peek_frame: *mut AVFrame,
66+
/// `true` si `peek_frame` porte une frame décodée en attente de `commit_peek`.
67+
has_peek: bool,
6168
}
6269

6370
/// Libère toutes les ressources ffmpeg. `Drop` ne peut pas faillir ; on
@@ -78,6 +85,9 @@ impl Drop for SwDecoder {
7885
if !self.pkt.is_null() {
7986
av_packet_free(&mut self.pkt);
8087
}
88+
if !self.peek_frame.is_null() {
89+
av_frame_free(&mut self.peek_frame);
90+
}
8191
}
8292
}
8393
}
@@ -177,7 +187,8 @@ impl SwDecoder {
177187
};
178188
let pkt = av_packet_alloc();
179189
let frame = av_frame_alloc();
180-
if pkt.is_null() || frame.is_null() {
190+
let peek_frame = av_frame_alloc();
191+
if pkt.is_null() || frame.is_null() || peek_frame.is_null() {
181192
avcodec_free_context(&mut dec);
182193
avformat_close_input(&mut fmt);
183194
bail!("av_packet_alloc/av_frame_alloc (pompage sequentiel)");
@@ -192,6 +203,8 @@ impl SwDecoder {
192203
frame,
193204
sent_eof: false,
194205
cur_pts: None,
206+
peek_frame,
207+
has_peek: false,
195208
})
196209
}
197210

@@ -201,21 +214,33 @@ impl SwDecoder {
201214
/// seek PAS : le decodeur garde son etat, donc une lecture sequentielle coute
202215
/// UN packet par frame au lieu d'un re-parcours de demi-GOP.
203216
pub unsafe fn next_frame(&mut self) -> Result<*mut AVFrame> {
217+
if self.has_peek {
218+
return self.commit_peek();
219+
}
220+
if !self.receive_into(self.frame)? {
221+
return Ok(ptr::null_mut());
222+
}
223+
let pts = (*self.frame).best_effort_timestamp;
224+
self.cur_pts = if pts == i64::MIN { None } else { Some(pts) };
225+
Ok(self.frame)
226+
}
227+
228+
/// Décode dans `into` (buffer courant ou de lookahead) jusqu'à obtenir une frame ou
229+
/// l'EOF — cf. `pipeline_macos::Decoder::receive_into` pour la justification.
230+
unsafe fn receive_into(&mut self, into: *mut AVFrame) -> Result<bool> {
204231
loop {
205-
let r = avcodec_receive_frame(self.dec, self.frame);
232+
let r = avcodec_receive_frame(self.dec, into);
206233
if r == 0 {
207-
let pts = (*self.frame).best_effort_timestamp;
208-
self.cur_pts = if pts == i64::MIN { None } else { Some(pts) };
209-
return Ok(self.frame);
234+
return Ok(true);
210235
}
211236
if r == AVERROR_EOF {
212-
return Ok(ptr::null_mut());
237+
return Ok(false);
213238
}
214239
if r != AVERROR_EAGAIN {
215240
bail!("avcodec_receive_frame: {r}");
216241
}
217242
if self.sent_eof {
218-
return Ok(ptr::null_mut());
243+
return Ok(false);
219244
}
220245
let rr = av_read_frame(self.fmt, self.pkt);
221246
if rr < 0 {
@@ -240,6 +265,41 @@ impl SwDecoder {
240265
}
241266
}
242267

268+
/// Décode la prochaine frame dans le buffer de lookahead et renvoie son temps.
269+
/// Cf. `pipeline_macos::Decoder::peek_next_time_sec`.
270+
pub(crate) unsafe fn peek_next_time_sec(&mut self) -> Result<NextFrameTime> {
271+
if !self.has_peek {
272+
if !self.receive_into(self.peek_frame)? {
273+
return Ok(NextFrameTime::Eof);
274+
}
275+
self.has_peek = true;
276+
}
277+
let pts = (*self.peek_frame).best_effort_timestamp;
278+
// Sans pts ni time_base exploitables on ne PEUT pas dire si la frame est due :
279+
// `Unknown`, et non `0.0` — qui passait pour « due » à tous les coups.
280+
Ok(if pts == i64::MIN || self.stream_timebase <= 0.0 {
281+
NextFrameTime::Unknown
282+
} else {
283+
NextFrameTime::At(pts as f64 * self.stream_timebase)
284+
})
285+
}
286+
287+
/// Promeut la frame de lookahead au rang de frame courante. Cf.
288+
/// `pipeline_macos::Decoder::commit_peek`.
289+
pub(crate) unsafe fn commit_peek(&mut self) -> Result<*mut AVFrame> {
290+
// `bail!` et non `debug_assert!` : compilée en release, l'assertion disparaissait
291+
// et l'échange promouvait un `AVFrame` jamais rempli, avec un
292+
// `best_effort_timestamp` indéterminé, jusque dans le chemin de présentation.
293+
if !self.has_peek {
294+
bail!("commit_peek sans peek_next_time_sec préalable");
295+
}
296+
std::mem::swap(&mut self.frame, &mut self.peek_frame);
297+
self.has_peek = false;
298+
let pts = (*self.frame).best_effort_timestamp;
299+
self.cur_pts = if pts == i64::MIN { None } else { Some(pts) };
300+
Ok(self.frame)
301+
}
302+
243303
/// Temps source (secondes) de la derniere frame rendue par `next_frame` /
244304
/// `decode_at`, tire du pts REEL et non d'un compteur d'index.
245305
pub fn cur_time_sec(&self) -> Option<f64> {
@@ -263,6 +323,8 @@ impl SwDecoder {
263323
/// `AVERROR_INVALIDDATA` plutôt que de paniquer : la prochaine itération
264324
/// lira le packet complet suivant.
265325
pub unsafe fn decode_at(&mut self, frame_idx: u32) -> Result<*mut AVFrame> {
326+
// Tout seek invalide un éventuel peek en attente — cf. pipeline_macos::Decoder::seek_to.
327+
self.has_peek = false;
266328
let fps = self.fps;
267329
let target_ts = (frame_idx as f64 / fps) * 1_000_000.0; // AV_TIME_BASE = µs
268330
// `AVSEEK_FLAG_BACKWARD` vaut 1, pas 4 — 4 est `AVSEEK_FLAG_ANY`. La constante

0 commit comments

Comments
 (0)