Que fait Vidéo de Compte à Rebours ?
Créez un compte à rebours qui se termine exactement au moment prévu. L'ensemble repose sur une seule idée : dériver le temps restant à partir de l'image (ou d'un timestamp monotone), puis formater et animer ce nombre — ne jamais compter avec un setInterval. Pour les écrans "À venir" pour stream, les comptes à rebours de lancement ou de vente, et les arrière-plans animés en boucle.
Quand l'utiliser
- Écrans "À venir" / "de retour dans un instant" pour les streams en direct avec un minuteur de 5 à 10 min.
- Comptes à rebours de lancement et de vente éclair (jj:hh:mm:ss jusqu'à une date cible ; mm:ss pour l'urgence).
- Tout compteur animé où le nombre doit atteindre zéro exactement sur l'image prévue.
La règle d'or : piloter le nombre par l'horloge, jamais par un tick
Un setInterval(fn, 1000) dérive — la fonction de rappel se déclenche au moins 1000 ms plus tard, jamais exactement, et s'accumule lorsque l'onglet est ralenti ou qu'une image de rendu est longue. Des pertes d'environ 1 seconde par minute sont courantes. La solution est la même pour la vidéo rendue et le web en direct : calculer le temps restant comme une fonction pure d'une horloge fiable, de sorte qu'une image tardive se corrige d'elle-même sur l'image suivante au lieu d'accumuler l'erreur.
| Contexte | Horloge fiable | Temps restant |
|---|---|---|
| Rendu Remotion | useCurrentFrame() | total − image/fps |
| Web (page en direct) | performance.now() ou Date.now() | cibleMs − maintenant |
| Vers une date fixe | Date.now() | timestampCible − Date.now() |
Ne stockez jamais le temps restant dans un état et ne le décrémentez pas. Recalculez-le à chaque image.
Compte à rebours frame-accurate (Remotion → MP4)
useCurrentFrame() est déterministe : l'image N est toujours rendue exactement à N/fps secondes. Soustrayez du total et le minuteur est sans dérive par construction.
import { useCurrentFrame, useVideoConfig } from "remotion";
export const Countdown: React.FC<{ durationInSeconds: number }> = ({
durationInSeconds,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// secondes restantes, bloquées à 0 — ceil pour que "1" s'affiche pendant sa seconde complète
const elapsed = frame / fps;
const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));
return <div style={{ fontVariantNumeric: "tabular-nums" }}>{formatMMSS(remaining)}</div>;
};
durationInSeconds est une prop, pas une constante — la même composition rend un minuteur de 5 minutes pour un stream ou un minuteur de 30 secondes pour une vente en changeant une seule entrée. Définissez durationInFrames de la composition sur durationInSeconds * fps pour que la vidéo ait exactement la durée du compte à rebours.
Minuteur Web (page en direct, requestAnimationFrame)
Pour une page en direct, pilotez une boucle rAF avec performance.now() et ne repeignez que lorsque la seconde affichée change (pas de travail 60 fois/seconde pour un nombre à 1 Hz) :
const start = performance.now();
let lastShown = -1;
function tick(now) {
const remaining = Math.max(0, Math.ceil(duration - (now - start) / 1000));
if (remaining !== lastShown) { render(remaining); lastShown = remaining; }
if (remaining > 0) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
Compter jusqu'à une date fixe suit la même idée avec Date.now() : Math.max(0, cible - Date.now()). Voir references/countdown-cookbook.md pour la version complète avec gestion des fuseaux horaires.
Formatage du temps
Divisez un entier de secondes restantes en champs avec division + modulo, puis ajoutez des zéros. Choisissez le format adapté à la durée — n'affichez jamais 00j 00h 05m quand seules les minutes importent.
const pad = (n) => String(n).padStart(2, "0");
const formatMMSS = (s) => `${pad(Math.floor(s / 60))}:${pad(s % 60)}`;
const formatDHMS = (s) => {
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60), sec = s % 60;
return `${pad(d)}:${pad(h)}:${pad(m)}:${pad(sec)}`;
};
| Format | Utilisation | Masquer les champs quand |
|---|---|---|
mm:ss | stream "À venir", minuteurs courts (<1h) | — |
hh:mm:ss | vente éclair, événement le jour même | masquer jj tant qu'à 0 |
jj:hh:mm:ss | lancement / compte à rebours multi-jours | masquer les unités à 0 pour une lecture plus claire |
Définissez toujours font-variant-numeric: tabular-nums (ou une police à chiffres de largeur fixe) pour que les chiffres conservent une largeur égale et que la mise en page ne saute pas lorsque les nombres changent.
Transitions de roue / bascule de chiffres
Un simple échange de nombres paraît bon marché ; l'aspect "odomètre" qui tourne vend le compte à rebours. Rendez chaque chiffre sous forme d'une bande verticale 0–9 et translatez-la de -chiffre × hauteurChiffre. Animez la position, pas le contenu textuel, de sorte que le changement soit un glissement, pas un saut.
.reel { height: 1em; overflow: hidden; }
.reel .col { transition: transform .45s cubic-bezier(.22,1,.36,1); }
/* col contient 0..9 empilés ; décaler de -valeur em */
Dans Remotion, pilotez le décalage de la bande en fonction de l'image au lieu d'une transition CSS (pour qu'il soit déterministe). Le style carte à bascule (moitié supérieure se replie sur la moitié inférieure) et la roue par chiffre sont tous deux détaillés, avec des composants exécutables, dans references/digit-flip.md.
Arrière-plan en boucle
Les écrans de stream durent des minutes, donc l'arrière-plan doit boucler de manière transparente. Construisez un mouvement sur un cycle qui se divise exactement dans la timeline (par exemple un dégradé ou une dérive de particules dont la période est imagesBoucle), et lisez-le avec image % imagesBoucle pour qu'il n'y ait pas de couture visible à la jonction. Gardez-le à faible contraste et lent — le nombre est le sujet ; l'arrière-plan est l'ambiance. Recettes dans references/countdown-cookbook.md.
Liste de vérification de sortie
- Temps restant recalculé à partir de
useCurrentFrame()/performance.now()à chaque image — pas de décrémentation avecsetInterval. - Le minuteur atteint
00:00exactement sur l'image prévue ;durationInFrames === durationInSeconds × fps. - La durée est une prop d'entrée, pas une constante codée en dur.
- Les chiffres utilisent
tabular-nums; la mise en page ne se déplace pas lorsque les nombres changent. - Le format correspond à la durée (mm:ss vs jj:hh:mm:ss) ; les unités à zéro en tête sont supprimées lorsqu'elles distraient.
- La période de boucle de l'arrière-plan divise la timeline de manière égale ; pas de couture à la jonction.
Livrer et vérifier (images fixes rendues → MP4)
Aide empaquetée (
scripts/) : assemblez vos images fixes avecscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png, puis vérifiez l'encodage avecscripts/probe-mp4.sh out.mp4 [LxH] [fps]. Voirscripts/README.md.
Le compte à rebours est livré sous forme de composition Remotion (<Composition> + schéma zod + defaultProps, durationInSeconds une prop) — le minuteur est une fonction pure de useCurrentFrame(), jamais setInterval / Date.now() / Math.random(). Définissez durationInFrames = durationInSeconds * fps. Livrable = out/*.mp4 + le projet. Le format par défaut est 9:16 vertical (1080×1920).
Boucle de vérification — rendu d'images fixes → inspection → encodage. Tout l'intérêt est la précision à l'image près, donc vérifiez la valeur du chiffre à des images exactes avant l'encodage.
# Images fixes au début / milieu / fin — AVEC LES PROPS LIVRÉES (la durée réelle que vous allez rendre)
npx remotion still Countdown out/f-start.png --frame=0 --props='{"durationInSeconds":300}'
npx remotion still Countdown out/f-mid.png --frame=N --props='{"durationInSeconds":300}'
npx remotion still Countdown out/f-end.png --frame=L --props='{"durationInSeconds":300}' # L = durationInFrames-1
# Inspectez chaque PNG — les chiffres doivent correspondre EXACTEMENT au calcul image/fps :
# - à l'image N, affiché = ceil(durationInSeconds - N/fps) (ex. 300s à 30fps, image 4500 → 150s → "02:30")
# - la dernière image affiche 00:00 (ou l'état de fin maintenu); pas de dérive, pas de décalage d'une seconde
# - tabular-nums : la mise en page ne saute pas entre les images
# - zone de sécurité 9:16 : le minuteur + le bloc de marque se situent en dehors des ~12% supérieurs et ~20-35% inférieurs de l'interface utilisateur et du rail d'action droit
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # encoder une fois les chiffres vérifiés
npx remotion render Countdown out/demo.gif --codec=gif # clip de preuve README
Utilisez npx remotion compositions pour confirmer durationInFrames/fps, puis calculez l'image pour n'importe quelle seconde que vous souhaitez vérifier.
Avant de terminer :
- Les images fixes sont rendues proprement à l'image 0, au milieu et à la dernière — aucune erreur.
- La valeur du chiffre à l'image N est exactement égale à
ceil(durationInSeconds - N/fps); la dernière image atteint 00:00 sur l'image prévue (pas de dérive / décalage). - Le minuteur + la marque restent dans la zone de sécurité 9:16 à chaque image vérifiée.
- Piloté par l'image uniquement — pas de
setInterval/Date.now()/ minuteurs ;tabular-numspour que la mise en page tienne. durationInSecondslivré (pas seulementdefaultProps) rend correctement ; MP4 encodé, GIF optionnel.
Fichiers de référence
references/countdown-cookbook.md— composant<Countdown>Remotion complet et exécutable (durée configurable, mm:ss / jj:hh:mm:ss, état de fin maintenu), le composant web de compte à rebours jusqu'à une date fixe avec gestion des fuseaux horaires, une mise en page d'écran de stream "À venir" (bloc de marque + poignées sociales + arrière-plan dégradé en boucle), et le motif d'arrière-plan en boucle transparente.references/digit-flip.md— deux composants d'animation de chiffres déterministes : la roue d'odomètre (bande verticale 0–9 pilotée par l'image) et l'effet carte à bascule (pli de la moitié supérieure), plus quand chacun convient pour un lancement, une vente ou un stream.