UNPKG

@remotion/player

Version:

React component for embedding a Remotion preview into your app

1,724 lines (1,691 loc) • 111 kB
"use client"; // src/icons.tsx import { jsx, jsxs } from "react/jsx-runtime"; var ICON_SIZE = 25; var fullscreenIconSize = 16; var PlayIcon = () => { return /* @__PURE__ */ jsx("svg", { width: ICON_SIZE, height: ICON_SIZE, viewBox: "0 0 25 25", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M8 6.375C7.40904 8.17576 7.06921 10.2486 7.01438 12.3871C6.95955 14.5255 7.19163 16.6547 7.6875 18.5625C9.95364 18.2995 12.116 17.6164 14.009 16.5655C15.902 15.5147 17.4755 14.124 18.6088 12.5C17.5158 10.8949 15.9949 9.51103 14.1585 8.45082C12.3222 7.3906 10.2174 6.68116 8 6.375Z", fill: "white", stroke: "white", strokeWidth: "6.25", strokeLinejoin: "round" }) }); }; var PauseIcon = () => { return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 100 100", width: ICON_SIZE, height: ICON_SIZE, children: [ /* @__PURE__ */ jsx("rect", { x: "25", y: "20", width: "20", height: "60", fill: "#fff", ry: "5", rx: "5" }), /* @__PURE__ */ jsx("rect", { x: "55", y: "20", width: "20", height: "60", fill: "#fff", ry: "5", rx: "5" }) ] }); }; var FullscreenIcon = ({ isFullscreen }) => { const strokeWidth = 6; const viewSize = 32; const out = isFullscreen ? 0 : strokeWidth / 2; const middleInset = isFullscreen ? strokeWidth * 1.6 : strokeWidth / 2; const inset = isFullscreen ? strokeWidth * 1.6 : strokeWidth * 2; return /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${viewSize} ${viewSize}`, height: fullscreenIconSize, width: fullscreenIconSize, children: [ /* @__PURE__ */ jsx("path", { d: ` M ${out} ${inset} L ${middleInset} ${middleInset} L ${inset} ${out} `, stroke: "#fff", strokeWidth, fill: "none" }), /* @__PURE__ */ jsx("path", { d: ` M ${viewSize - out} ${inset} L ${viewSize - middleInset} ${middleInset} L ${viewSize - inset} ${out} `, stroke: "#fff", strokeWidth, fill: "none" }), /* @__PURE__ */ jsx("path", { d: ` M ${out} ${viewSize - inset} L ${middleInset} ${viewSize - middleInset} L ${inset} ${viewSize - out} `, stroke: "#fff", strokeWidth, fill: "none" }), /* @__PURE__ */ jsx("path", { d: ` M ${viewSize - out} ${viewSize - inset} L ${viewSize - middleInset} ${viewSize - middleInset} L ${viewSize - inset} ${viewSize - out} `, stroke: "#fff", strokeWidth, fill: "none" }) ] }); }; var VolumeOffIcon = () => { return /* @__PURE__ */ jsx("svg", { width: ICON_SIZE, height: ICON_SIZE, viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { d: "M3.63 3.63a.996.996 0 000 1.41L7.29 8.7 7 9H4c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h3l3.29 3.29c.63.63 1.71.18 1.71-.71v-4.17l4.18 4.18c-.49.37-1.02.68-1.6.91-.36.15-.58.53-.58.92 0 .72.73 1.18 1.39.91.8-.33 1.55-.77 2.22-1.31l1.34 1.34a.996.996 0 101.41-1.41L5.05 3.63c-.39-.39-1.02-.39-1.42 0zM19 12c0 .82-.15 1.61-.41 2.34l1.53 1.53c.56-1.17.88-2.48.88-3.87 0-3.83-2.4-7.11-5.78-8.4-.59-.23-1.22.23-1.22.86v.19c0 .38.25.71.61.85C17.18 6.54 19 9.06 19 12zm-8.71-6.29l-.17.17L12 7.76V6.41c0-.89-1.08-1.33-1.71-.7zM16.5 12A4.5 4.5 0 0014 7.97v1.79l2.48 2.48c.01-.08.02-.16.02-.24z", fill: "#fff" }) }); }; var VolumeOnIcon = () => { return /* @__PURE__ */ jsx("svg", { width: ICON_SIZE, height: ICON_SIZE, viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { d: "M3 10v4c0 .55.45 1 1 1h3l3.29 3.29c.63.63 1.71.18 1.71-.71V6.41c0-.89-1.08-1.34-1.71-.71L7 9H4c-.55 0-1 .45-1 1zm13.5 2A4.5 4.5 0 0014 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 4.45v.2c0 .38.25.71.6.85C17.18 6.53 19 9.06 19 12s-1.82 5.47-4.4 6.5c-.36.14-.6.47-.6.85v.2c0 .63.63 1.07 1.21.85C18.6 19.11 21 15.84 21 12s-2.4-7.11-5.79-8.4c-.58-.23-1.21.22-1.21.85z", fill: "#fff" }) }); }; // src/BufferingIndicator.tsx import { jsx as jsx2, jsxs as jsxs2, Fragment } from "react/jsx-runtime"; var className = "__remotion_buffering_indicator"; var remotionBufferingAnimation = "__remotion_buffering_animation"; var playerStyle = { width: ICON_SIZE, height: ICON_SIZE, overflow: "hidden", lineHeight: "normal", fontSize: "inherit" }; var studioStyle = { width: 14, height: 14, overflow: "hidden", lineHeight: "normal", fontSize: "inherit" }; var BufferingIndicator = ({ type }) => { const style = type === "player" ? playerStyle : studioStyle; return /* @__PURE__ */ jsxs2(Fragment, { children: [ /* @__PURE__ */ jsx2("style", { type: "text/css", children: ` @keyframes ${remotionBufferingAnimation} { 0% { rotate: 0deg; } 100% { rotate: 360deg; } } .${className} { animation: ${remotionBufferingAnimation} 1s linear infinite; } ` }), /* @__PURE__ */ jsx2("div", { style, children: /* @__PURE__ */ jsx2("svg", { viewBox: type === "player" ? "0 0 22 22" : "0 0 18 18", style, className, children: /* @__PURE__ */ jsx2("path", { d: type === "player" ? "M 11 4 A 7 7 0 0 1 15.1145 16.66312" : "M 9 2 A 7 7 0 0 1 13.1145 14.66312", stroke: "white", strokeLinecap: "round", fill: "none", strokeWidth: 3 }) }) }) ] }); }; // src/calculate-scale.ts import { Internals } from "remotion"; // src/utils/calculate-player-size.ts var calculatePlayerSize = ({ currentSize, width, height, compositionWidth, compositionHeight }) => { if (width !== undefined && height === undefined) { return { aspectRatio: [compositionWidth, compositionHeight].join("/") }; } if (height !== undefined && width === undefined) { return { aspectRatio: [compositionWidth, compositionHeight].join("/") }; } if (!currentSize) { return { width: compositionWidth, height: compositionHeight }; } return { width: compositionWidth, height: compositionHeight }; }; // src/calculate-scale.ts var calculateCanvasTransformation = ({ previewSize, compositionWidth, compositionHeight, canvasSize }) => { const scale = Internals.calculateScale({ canvasSize, compositionHeight, compositionWidth, previewSize }); const correction = 0 - (1 - scale) / 2; const xCorrection = correction * compositionWidth; const yCorrection = correction * compositionHeight; const width = compositionWidth * scale; const height = compositionHeight * scale; const centerX = canvasSize.width / 2 - width / 2; const centerY = canvasSize.height / 2 - height / 2; return { centerX, centerY, xCorrection, yCorrection, scale }; }; var calculateOuterStyle = ({ config, style, canvasSize, overflowVisible, layout }) => { if (!config) { return {}; } return { position: "relative", overflow: overflowVisible ? "visible" : "hidden", ...calculatePlayerSize({ compositionHeight: config.height, compositionWidth: config.width, currentSize: canvasSize, height: style?.height, width: style?.width }), opacity: layout ? 1 : 0, ...style }; }; var calculateContainerStyle = ({ config, canvasSize, layout, scale, overflowVisible }) => { if (!config || !canvasSize || !layout) { return {}; } return { position: "absolute", width: config.width, height: config.height, display: "flex", transform: `scale(${scale})`, marginLeft: layout.xCorrection, marginTop: layout.yCorrection, overflow: overflowVisible ? "visible" : "hidden" }; }; var calculateOuter = ({ layout, scale, config, overflowVisible }) => { if (!layout || !config) { return {}; } const { centerX, centerY } = layout; return { width: config.width * scale, height: config.height * scale, display: "flex", flexDirection: "column", position: "absolute", left: centerX, top: centerY, overflow: overflowVisible ? "visible" : "hidden" }; }; // src/emitter-context.ts import React from "react"; var PlayerEventEmitterContext = React.createContext(undefined); var ThumbnailEmitterContext = React.createContext(undefined); // src/EmitterProvider.tsx import { useContext as useContext2, useEffect as useEffect2, useState } from "react"; import { Internals as Internals3 } from "remotion"; // src/event-emitter.ts class PlayerEmitter { listeners = { ended: [], error: [], pause: [], play: [], ratechange: [], scalechange: [], seeked: [], timeupdate: [], frameupdate: [], fullscreenchange: [], volumechange: [], mutechange: [], waiting: [], resume: [] }; addEventListener(name, callback) { this.listeners[name].push(callback); } removeEventListener(name, callback) { this.listeners[name] = this.listeners[name].filter((l) => l !== callback); } dispatchEvent(dispatchName, context) { this.listeners[dispatchName].forEach((callback) => { callback({ detail: context }); }); } dispatchSeek = (frame) => { this.dispatchEvent("seeked", { frame }); }; dispatchVolumeChange = (volume) => { this.dispatchEvent("volumechange", { volume }); }; dispatchPause = () => { this.dispatchEvent("pause", undefined); }; dispatchPlay = () => { this.dispatchEvent("play", undefined); }; dispatchEnded = () => { this.dispatchEvent("ended", undefined); }; dispatchRateChange = (playbackRate) => { this.dispatchEvent("ratechange", { playbackRate }); }; dispatchScaleChange = (scale) => { this.dispatchEvent("scalechange", { scale }); }; dispatchError = (error) => { this.dispatchEvent("error", { error }); }; dispatchTimeUpdate = (event) => { this.dispatchEvent("timeupdate", event); }; dispatchFrameUpdate = (event) => { this.dispatchEvent("frameupdate", event); }; dispatchFullscreenChange = (event) => { this.dispatchEvent("fullscreenchange", event); }; dispatchMuteChange = (event) => { this.dispatchEvent("mutechange", event); }; dispatchWaiting = (event) => { this.dispatchEvent("waiting", event); }; dispatchResume = (event) => { this.dispatchEvent("resume", event); }; } class ThumbnailEmitter { listeners = { error: [], waiting: [], resume: [] }; addEventListener(name, callback) { this.listeners[name].push(callback); } removeEventListener(name, callback) { this.listeners[name] = this.listeners[name].filter((l) => l !== callback); } dispatchEvent(dispatchName, context) { this.listeners[dispatchName].forEach((callback) => { callback({ detail: context }); }); } dispatchError = (error) => { this.dispatchEvent("error", { error }); }; dispatchWaiting = (event) => { this.dispatchEvent("waiting", event); }; dispatchResume = (event) => { this.dispatchEvent("resume", event); }; } // src/use-buffer-state-emitter.ts import { useContext, useEffect } from "react"; import { Internals as Internals2 } from "remotion"; var useBufferStateEmitter = (emitter) => { const bufferManager = useContext(Internals2.BufferingContextReact); if (!bufferManager) { throw new Error("BufferingContextReact not found"); } useEffect(() => { const clear1 = bufferManager.listenForBuffering(() => { bufferManager.buffering.current = true; emitter.dispatchWaiting({}); }); const clear2 = bufferManager.listenForResume(() => { bufferManager.buffering.current = false; emitter.dispatchResume({}); }); return () => { clear1.remove(); clear2.remove(); }; }, [bufferManager, emitter]); }; // src/EmitterProvider.tsx import { jsx as jsx3 } from "react/jsx-runtime"; var PlayerEmitterProvider = ({ children, currentPlaybackRate }) => { const [emitter] = useState(() => new PlayerEmitter); const bufferManager = useContext2(Internals3.BufferingContextReact); if (!bufferManager) { throw new Error("BufferingContextReact not found"); } useEffect2(() => { if (currentPlaybackRate) { emitter.dispatchRateChange(currentPlaybackRate); } }, [emitter, currentPlaybackRate]); useBufferStateEmitter(emitter); return /* @__PURE__ */ jsx3(PlayerEventEmitterContext.Provider, { value: emitter, children }); }; // src/use-frame-imperative.ts import { useCallback, useRef } from "react"; import { Internals as Internals4 } from "remotion"; var useFrameImperative = () => { const frame = Internals4.Timeline.useTimelinePosition(); const frameRef = useRef(frame); frameRef.current = frame; const getCurrentFrame = useCallback(() => { return frameRef.current; }, []); return getCurrentFrame; }; // src/use-hover-state.ts import { useEffect as useEffect3, useState as useState2 } from "react"; var useHoverState = (ref, hideControlsWhenPointerDoesntMove) => { const [hovered, setHovered] = useState2(false); useEffect3(() => { const { current } = ref; if (!current) { return; } let hoverTimeout; const addHoverTimeout = () => { if (hideControlsWhenPointerDoesntMove) { clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => { setHovered(false); }, hideControlsWhenPointerDoesntMove === true ? 3000 : hideControlsWhenPointerDoesntMove); } }; const onHover = () => { setHovered(true); addHoverTimeout(); }; const onLeave = () => { setHovered(false); clearTimeout(hoverTimeout); }; const onMove = () => { setHovered(true); addHoverTimeout(); }; current.addEventListener("mouseenter", onHover); current.addEventListener("mouseleave", onLeave); current.addEventListener("mousemove", onMove); return () => { current.removeEventListener("mouseenter", onHover); current.removeEventListener("mouseleave", onLeave); current.removeEventListener("mousemove", onMove); clearTimeout(hoverTimeout); }; }, [hideControlsWhenPointerDoesntMove, ref]); return hovered; }; // src/use-playback.ts import { useContext as useContext4, useEffect as useEffect6, useRef as useRef4 } from "react"; import { Internals as Internals6 } from "remotion"; // src/browser-mediasession.ts import { useEffect as useEffect4 } from "react"; // src/use-player.ts import { useCallback as useCallback2, useContext as useContext3, useMemo, useRef as useRef2, useState as useState3 } from "react"; import { Internals as Internals5 } from "remotion"; var usePlayer = () => { const [playing, setPlaying, imperativePlaying] = Internals5.Timeline.usePlayingState(); const [hasPlayed, setHasPlayed] = useState3(false); const frame = Internals5.Timeline.useTimelinePosition(); const playStart = useRef2(frame); const setFrame = Internals5.Timeline.useTimelineSetFrame(); const setTimelinePosition = Internals5.Timeline.useTimelineSetFrame(); const audioContext = useContext3(Internals5.SharedAudioContext); const { audioAndVideoTags } = useContext3(Internals5.Timeline.TimelineContext); const frameRef = useRef2(frame); frameRef.current = frame; const video = Internals5.useVideo(); const config = Internals5.useUnsafeVideoConfig(); const emitter = useContext3(PlayerEventEmitterContext); const lastFrame = (config?.durationInFrames ?? 1) - 1; const isLastFrame = frame === lastFrame; const isFirstFrame = frame === 0; if (!emitter) { throw new TypeError("Expected Player event emitter context"); } const bufferingContext = useContext3(Internals5.BufferingContextReact); if (!bufferingContext) { throw new Error("Missing the buffering context. Most likely you have a Remotion version mismatch."); } const { buffering } = bufferingContext; const seek = useCallback2((newFrame) => { if (video?.id) { setTimelinePosition((c) => ({ ...c, [video.id]: newFrame })); } frameRef.current = newFrame; emitter.dispatchSeek(newFrame); }, [emitter, setTimelinePosition, video?.id]); const play = useCallback2((e) => { if (imperativePlaying.current) { return; } setHasPlayed(true); if (isLastFrame) { seek(0); } if (audioContext && audioContext.numberOfAudioTags > 0 && e) { audioContext.playAllAudios(); } audioAndVideoTags.current.forEach((a) => a.play("player play() was called and playing audio from a click")); imperativePlaying.current = true; setPlaying(true); playStart.current = frameRef.current; emitter.dispatchPlay(); }, [ imperativePlaying, isLastFrame, audioContext, setPlaying, emitter, seek, audioAndVideoTags ]); const pause = useCallback2(() => { if (imperativePlaying.current) { imperativePlaying.current = false; setPlaying(false); emitter.dispatchPause(); } }, [emitter, imperativePlaying, setPlaying]); const pauseAndReturnToPlayStart = useCallback2(() => { if (imperativePlaying.current) { imperativePlaying.current = false; frameRef.current = playStart.current; if (config) { setTimelinePosition((c) => ({ ...c, [config.id]: playStart.current })); setPlaying(false); emitter.dispatchPause(); } } }, [config, emitter, imperativePlaying, setPlaying, setTimelinePosition]); const videoId = video?.id; const frameBack = useCallback2((frames) => { if (!videoId) { return null; } if (imperativePlaying.current) { return; } setFrame((c) => { const prevFrame = c[videoId] ?? window.remotion_initialFrame ?? 0; const newFrame = Math.max(0, prevFrame - frames); if (prevFrame === newFrame) { return c; } return { ...c, [videoId]: newFrame }; }); }, [imperativePlaying, setFrame, videoId]); const frameForward = useCallback2((frames) => { if (!videoId) { return null; } if (imperativePlaying.current) { return; } setFrame((c) => { const prevFrame = c[videoId] ?? window.remotion_initialFrame ?? 0; const newFrame = Math.min(lastFrame, prevFrame + frames); if (prevFrame === newFrame) { return c; } return { ...c, [videoId]: newFrame }; }); }, [videoId, imperativePlaying, lastFrame, setFrame]); const getCurrentFrame = useFrameImperative(); const toggle = useCallback2((e) => { if (imperativePlaying.current) { pause(); } else { play(e); } }, [imperativePlaying, pause, play]); const returnValue = useMemo(() => { return { frameBack, frameForward, isLastFrame, emitter, playing, play, pause, seek, isFirstFrame, getCurrentFrame, isPlaying: () => imperativePlaying.current, isBuffering: () => buffering.current, pauseAndReturnToPlayStart, hasPlayed, remotionInternal_currentFrameRef: frameRef, toggle }; }, [ buffering, emitter, frameBack, frameForward, getCurrentFrame, hasPlayed, imperativePlaying, isFirstFrame, isLastFrame, pause, pauseAndReturnToPlayStart, play, playing, seek, toggle ]); return returnValue; }; // src/browser-mediasession.ts var useBrowserMediaSession = ({ browserMediaControlsBehavior, videoConfig, playbackRate }) => { const { playing, pause, play, emitter, getCurrentFrame, seek } = usePlayer(); useEffect4(() => { if (!navigator.mediaSession) { return; } if (browserMediaControlsBehavior.mode === "do-nothing") { return; } if (playing) { navigator.mediaSession.playbackState = "playing"; } else { navigator.mediaSession.playbackState = "paused"; } }, [browserMediaControlsBehavior.mode, playing]); useEffect4(() => { if (!navigator.mediaSession) { return; } if (browserMediaControlsBehavior.mode === "do-nothing") { return; } const onTimeUpdate = () => { if (!videoConfig) { return; } if (navigator.mediaSession) { navigator.mediaSession.setPositionState({ duration: videoConfig.durationInFrames / videoConfig.fps, playbackRate, position: getCurrentFrame() / videoConfig.fps }); } }; emitter.addEventListener("timeupdate", onTimeUpdate); return () => { emitter.removeEventListener("timeupdate", onTimeUpdate); }; }, [ browserMediaControlsBehavior.mode, emitter, getCurrentFrame, playbackRate, videoConfig ]); useEffect4(() => { if (!navigator.mediaSession) { return; } if (browserMediaControlsBehavior.mode === "do-nothing") { return; } navigator.mediaSession.setActionHandler("play", () => { if (browserMediaControlsBehavior.mode === "register-media-session") { play(); } }); navigator.mediaSession.setActionHandler("pause", () => { if (browserMediaControlsBehavior.mode === "register-media-session") { pause(); } }); navigator.mediaSession.setActionHandler("seekto", (event) => { if (browserMediaControlsBehavior.mode === "register-media-session" && event.seekTime !== undefined && videoConfig) { seek(Math.round(event.seekTime * videoConfig.fps)); } }); navigator.mediaSession.setActionHandler("seekbackward", () => { if (browserMediaControlsBehavior.mode === "register-media-session" && videoConfig) { seek(Math.max(0, Math.round((getCurrentFrame() - 10) * videoConfig.fps))); } }); navigator.mediaSession.setActionHandler("seekforward", () => { if (browserMediaControlsBehavior.mode === "register-media-session" && videoConfig) { seek(Math.max(videoConfig.durationInFrames - 1, Math.round((getCurrentFrame() + 10) * videoConfig.fps))); } }); navigator.mediaSession.setActionHandler("previoustrack", () => { if (browserMediaControlsBehavior.mode === "register-media-session") { seek(0); } }); return () => { navigator.mediaSession.metadata = null; navigator.mediaSession.setActionHandler("play", null); navigator.mediaSession.setActionHandler("pause", null); navigator.mediaSession.setActionHandler("seekto", null); navigator.mediaSession.setActionHandler("seekbackward", null); navigator.mediaSession.setActionHandler("seekforward", null); navigator.mediaSession.setActionHandler("previoustrack", null); }; }, [ browserMediaControlsBehavior.mode, getCurrentFrame, pause, play, seek, videoConfig ]); }; // src/calculate-next-frame.ts var calculateNextFrame = ({ time, currentFrame: startFrame, playbackSpeed, fps, actualLastFrame, actualFirstFrame, framesAdvanced, shouldLoop }) => { const op = playbackSpeed < 0 ? Math.ceil : Math.floor; const framesToAdvance = op(time * playbackSpeed / (1000 / fps)) - framesAdvanced; const nextFrame = framesToAdvance + startFrame; const isCurrentFrameOutside = startFrame > actualLastFrame || startFrame < actualFirstFrame; const isNextFrameOutside = nextFrame > actualLastFrame || nextFrame < actualFirstFrame; const hasEnded = !shouldLoop && isNextFrameOutside && !isCurrentFrameOutside; if (playbackSpeed > 0) { if (isNextFrameOutside) { return { nextFrame: actualFirstFrame, framesToAdvance, hasEnded }; } return { nextFrame, framesToAdvance, hasEnded }; } if (isNextFrameOutside) { return { nextFrame: actualLastFrame, framesToAdvance, hasEnded }; } return { nextFrame, framesToAdvance, hasEnded }; }; // src/is-backgrounded.ts import { useEffect as useEffect5, useRef as useRef3 } from "react"; var getIsBackgrounded = () => { if (typeof document === "undefined") { return false; } return document.visibilityState === "hidden"; }; var useIsBackgrounded = () => { const isBackgrounded = useRef3(getIsBackgrounded()); useEffect5(() => { const onVisibilityChange = () => { isBackgrounded.current = getIsBackgrounded(); }; document.addEventListener("visibilitychange", onVisibilityChange); return () => { document.removeEventListener("visibilitychange", onVisibilityChange); }; }, []); return isBackgrounded; }; // src/use-playback.ts var usePlayback = ({ loop, playbackRate, moveToBeginningWhenEnded, inFrame, outFrame, browserMediaControlsBehavior, getCurrentFrame }) => { const config = Internals6.useUnsafeVideoConfig(); const frame = Internals6.Timeline.useTimelinePosition(); const { playing, pause, emitter } = usePlayer(); const setFrame = Internals6.Timeline.useTimelineSetFrame(); const buffering = useRef4(null); const isBackgroundedRef = useIsBackgrounded(); const lastTimeUpdateEvent = useRef4(null); const context = useContext4(Internals6.BufferingContextReact); if (!context) { throw new Error("Missing the buffering context. Most likely you have a Remotion version mismatch."); } useBrowserMediaSession({ browserMediaControlsBehavior, playbackRate, videoConfig: config }); useEffect6(() => { const onBufferClear = context.listenForBuffering(() => { buffering.current = performance.now(); }); const onResumeClear = context.listenForResume(() => { buffering.current = null; }); return () => { onBufferClear.remove(); onResumeClear.remove(); }; }, [context]); useEffect6(() => { if (!config) { return; } if (!playing) { return; } let hasBeenStopped = false; let reqAnimFrameCall = null; let startedTime = performance.now(); let framesAdvanced = 0; const cancelQueuedFrame = () => { if (reqAnimFrameCall !== null) { if (reqAnimFrameCall.type === "raf") { cancelAnimationFrame(reqAnimFrameCall.id); } else { clearTimeout(reqAnimFrameCall.id); } } }; const stop = () => { hasBeenStopped = true; cancelQueuedFrame(); }; const callback = () => { const time = performance.now() - startedTime; const actualLastFrame = outFrame ?? config.durationInFrames - 1; const actualFirstFrame = inFrame ?? 0; const currentFrame = getCurrentFrame(); const { nextFrame, framesToAdvance, hasEnded } = calculateNextFrame({ time, currentFrame, playbackSpeed: playbackRate, fps: config.fps, actualFirstFrame, actualLastFrame, framesAdvanced, shouldLoop: loop }); framesAdvanced += framesToAdvance; if (nextFrame !== getCurrentFrame() && (!hasEnded || moveToBeginningWhenEnded)) { setFrame((c) => ({ ...c, [config.id]: nextFrame })); } if (hasEnded) { stop(); pause(); emitter.dispatchEnded(); return; } if (!hasBeenStopped) { queueNextFrame(); } }; const queueNextFrame = () => { if (buffering.current) { const stopListening = context.listenForResume(() => { stopListening.remove(); if (hasBeenStopped) { return; } startedTime = performance.now(); framesAdvanced = 0; callback(); }); return; } if (isBackgroundedRef.current) { reqAnimFrameCall = { type: "timeout", id: setTimeout(callback, 1000 / config.fps) }; } else { reqAnimFrameCall = { type: "raf", id: requestAnimationFrame(callback) }; } }; queueNextFrame(); const onVisibilityChange = () => { if (document.visibilityState === "visible") { return; } cancelQueuedFrame(); callback(); }; window.addEventListener("visibilitychange", onVisibilityChange); return () => { window.removeEventListener("visibilitychange", onVisibilityChange); stop(); }; }, [ config, loop, pause, playing, setFrame, emitter, playbackRate, inFrame, outFrame, moveToBeginningWhenEnded, isBackgroundedRef, getCurrentFrame, buffering, context ]); useEffect6(() => { const interval = setInterval(() => { if (lastTimeUpdateEvent.current === getCurrentFrame()) { return; } emitter.dispatchTimeUpdate({ frame: getCurrentFrame() }); lastTimeUpdateEvent.current = getCurrentFrame(); }, 250); return () => clearInterval(interval); }, [emitter, getCurrentFrame]); useEffect6(() => { emitter.dispatchFrameUpdate({ frame }); }, [emitter, frame]); }; // src/utils/use-element-size.ts import { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo2, useState as useState4 } from "react"; var elementSizeHooks = []; var updateAllElementsSizes = () => { for (const listener of elementSizeHooks) { listener(); } }; var useElementSize = (ref, options) => { const [size, setSize] = useState4(() => { if (!ref.current) { return null; } const rect = ref.current.getClientRects(); if (!rect[0]) { return null; } return { width: rect[0].width, height: rect[0].height, left: rect[0].x, top: rect[0].y, windowSize: { height: window.innerHeight, width: window.innerWidth } }; }); const observer = useMemo2(() => { if (typeof ResizeObserver === "undefined") { return null; } return new ResizeObserver((entries) => { const { contentRect, target } = entries[0]; const newSize = target.getClientRects(); if (!newSize?.[0]) { setSize(null); return; } const probableCssParentScale = contentRect.width === 0 ? 1 : newSize[0].width / contentRect.width; const width = options.shouldApplyCssTransforms ? newSize[0].width : newSize[0].width * (1 / probableCssParentScale); const height = options.shouldApplyCssTransforms ? newSize[0].height : newSize[0].height * (1 / probableCssParentScale); setSize((prevState) => { const isSame = prevState && prevState.width === width && prevState.height === height && prevState.left === newSize[0].x && prevState.top === newSize[0].y && prevState.windowSize.height === window.innerHeight && prevState.windowSize.width === window.innerWidth; if (isSame) { return prevState; } return { width, height, left: newSize[0].x, top: newSize[0].y, windowSize: { height: window.innerHeight, width: window.innerWidth } }; }); }); }, [options.shouldApplyCssTransforms]); const updateSize = useCallback3(() => { if (!ref.current) { return; } const rect = ref.current.getClientRects(); if (!rect[0]) { setSize(null); return; } setSize((prevState) => { const isSame = prevState && prevState.width === rect[0].width && prevState.height === rect[0].height && prevState.left === rect[0].x && prevState.top === rect[0].y && prevState.windowSize.height === window.innerHeight && prevState.windowSize.width === window.innerWidth; if (isSame) { return prevState; } return { width: rect[0].width, height: rect[0].height, left: rect[0].x, top: rect[0].y, windowSize: { height: window.innerHeight, width: window.innerWidth } }; }); }, [ref]); useEffect7(() => { if (!observer) { return; } const { current } = ref; if (current) { observer.observe(current); } return () => { if (current) { observer.unobserve(current); } }; }, [observer, ref, updateSize]); useEffect7(() => { if (!options.triggerOnWindowResize) { return; } window.addEventListener("resize", updateSize); return () => { window.removeEventListener("resize", updateSize); }; }, [options.triggerOnWindowResize, updateSize]); useEffect7(() => { elementSizeHooks.push(updateSize); return () => { elementSizeHooks = elementSizeHooks.filter((e) => e !== updateSize); }; }, [updateSize]); return useMemo2(() => { if (!size) { return null; } return { ...size, refresh: updateSize }; }, [size, updateSize]); }; // src/Player.tsx import { forwardRef as forwardRef2, useEffect as useEffect14, useImperativeHandle as useImperativeHandle2, useLayoutEffect, useMemo as useMemo14, useRef as useRef11, useState as useState13 } from "react"; import { Composition, Internals as Internals15 } from "remotion"; // src/PlayerUI.tsx import React10, { Suspense, forwardRef, useCallback as useCallback11, useContext as useContext6, useEffect as useEffect13, useImperativeHandle, useMemo as useMemo12, useRef as useRef10, useState as useState11 } from "react"; import { Internals as Internals11 } from "remotion"; // src/PlayerControls.tsx import { useCallback as useCallback8, useEffect as useEffect11, useMemo as useMemo9, useRef as useRef8, useState as useState10 } from "react"; // src/DefaultPlayPauseButton.tsx import { jsx as jsx4 } from "react/jsx-runtime"; var DefaultPlayPauseButton = ({ playing, buffering }) => { if (playing && buffering) { return /* @__PURE__ */ jsx4(BufferingIndicator, { type: "player" }); } if (playing) { return /* @__PURE__ */ jsx4(PauseIcon, {}); } return /* @__PURE__ */ jsx4(PlayIcon, {}); }; // src/MediaVolumeSlider.tsx import { useCallback as useCallback5, useMemo as useMemo4, useRef as useRef5, useState as useState6 } from "react"; import { Internals as Internals7 } from "remotion"; // src/render-volume-slider.tsx import React3, { useCallback as useCallback4, useMemo as useMemo3, useState as useState5 } from "react"; import { random } from "remotion"; import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime"; var KNOB_SIZE = 12; var BAR_HEIGHT = 5; var DefaultVolumeSlider = ({ volume, isVertical, onBlur, inputRef, setVolume }) => { const sliderContainer = useMemo3(() => { const paddingLeft = 5; const common = { paddingLeft, height: ICON_SIZE, width: VOLUME_SLIDER_WIDTH, display: "inline-flex", alignItems: "center" }; if (isVertical) { return { ...common, position: "absolute", transform: `rotate(-90deg) translateX(${VOLUME_SLIDER_WIDTH / 2 + ICON_SIZE / 2}px)` }; } return { ...common }; }, [isVertical]); const randomId = typeof React3.useId === "undefined" ? "volume-slider" : React3.useId(); const [randomClass] = useState5(() => `__remotion-volume-slider-${random(randomId)}`.replace(".", "")); const onVolumeChange = useCallback4((e) => { setVolume(parseFloat(e.target.value)); }, [setVolume]); const inputStyle = useMemo3(() => { const commonStyle = { WebkitAppearance: "none", backgroundColor: "rgba(255, 255, 255, 0.5)", borderRadius: BAR_HEIGHT / 2, cursor: "pointer", height: BAR_HEIGHT, width: VOLUME_SLIDER_WIDTH, backgroundImage: `linear-gradient( to right, white ${volume * 100}%, rgba(255, 255, 255, 0) ${volume * 100}% )` }; if (isVertical) { return { ...commonStyle, bottom: ICON_SIZE + VOLUME_SLIDER_WIDTH / 2 }; } return commonStyle; }, [isVertical, volume]); const sliderStyle = ` .${randomClass}::-webkit-slider-thumb { -webkit-appearance: none; background-color: white; border-radius: ${KNOB_SIZE / 2}px; box-shadow: 0 0 2px black; height: ${KNOB_SIZE}px; width: ${KNOB_SIZE}px; } .${randomClass}::-moz-range-thumb { -webkit-appearance: none; background-color: white; border-radius: ${KNOB_SIZE / 2}px; box-shadow: 0 0 2px black; height: ${KNOB_SIZE}px; width: ${KNOB_SIZE}px; } `; return /* @__PURE__ */ jsxs3("div", { style: sliderContainer, children: [ /* @__PURE__ */ jsx5("style", { dangerouslySetInnerHTML: { __html: sliderStyle } }), /* @__PURE__ */ jsx5("input", { ref: inputRef, "aria-label": "Change volume", className: randomClass, max: 1, min: 0, onBlur, onChange: onVolumeChange, step: 0.01, type: "range", value: volume, style: inputStyle }) ] }); }; var renderDefaultVolumeSlider = (props) => { return /* @__PURE__ */ jsx5(DefaultVolumeSlider, { ...props }); }; // src/MediaVolumeSlider.tsx import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime"; var VOLUME_SLIDER_WIDTH = 100; var MediaVolumeSlider = ({ displayVerticalVolumeSlider, renderMuteButton, renderVolumeSlider }) => { const [mediaMuted, setMediaMuted] = Internals7.useMediaMutedState(); const [mediaVolume, setMediaVolume] = Internals7.useMediaVolumeState(); const [focused, setFocused] = useState6(false); const parentDivRef = useRef5(null); const inputRef = useRef5(null); const hover = useHoverState(parentDivRef, false); const onBlur = useCallback5(() => { setTimeout(() => { if (inputRef.current && document.activeElement !== inputRef.current) { setFocused(false); } }, 10); }, []); const isVolume0 = mediaVolume === 0; const onClick = useCallback5(() => { if (isVolume0) { setMediaVolume(1); setMediaMuted(false); return; } setMediaMuted((mute) => !mute); }, [isVolume0, setMediaMuted, setMediaVolume]); const parentDivStyle = useMemo4(() => { return { display: "inline-flex", background: "none", border: "none", justifyContent: "center", alignItems: "center", touchAction: "none", ...displayVerticalVolumeSlider && { position: "relative" } }; }, [displayVerticalVolumeSlider]); const volumeContainer = useMemo4(() => { return { display: "inline", width: ICON_SIZE, height: ICON_SIZE, cursor: "pointer", appearance: "none", background: "none", border: "none", padding: 0 }; }, []); const renderDefaultMuteButton = useCallback5(({ muted, volume }) => { const isMutedOrZero = muted || volume === 0; return /* @__PURE__ */ jsx6("button", { "aria-label": isMutedOrZero ? "Unmute sound" : "Mute sound", title: isMutedOrZero ? "Unmute sound" : "Mute sound", onClick, onBlur, onFocus: () => setFocused(true), style: volumeContainer, type: "button", children: isMutedOrZero ? /* @__PURE__ */ jsx6(VolumeOffIcon, {}) : /* @__PURE__ */ jsx6(VolumeOnIcon, {}) }); }, [onBlur, onClick, volumeContainer]); const muteButton = useMemo4(() => { return renderMuteButton ? renderMuteButton({ muted: mediaMuted, volume: mediaVolume }) : renderDefaultMuteButton({ muted: mediaMuted, volume: mediaVolume }); }, [mediaMuted, mediaVolume, renderDefaultMuteButton, renderMuteButton]); const volumeSlider = useMemo4(() => { return (focused || hover) && !mediaMuted && !Internals7.isIosSafari() ? (renderVolumeSlider ?? renderDefaultVolumeSlider)({ isVertical: displayVerticalVolumeSlider, volume: mediaVolume, onBlur: () => setFocused(false), inputRef, setVolume: setMediaVolume }) : null; }, [ displayVerticalVolumeSlider, focused, hover, mediaMuted, mediaVolume, renderVolumeSlider, setMediaVolume ]); return /* @__PURE__ */ jsxs4("div", { ref: parentDivRef, style: parentDivStyle, children: [ muteButton, volumeSlider ] }); }; // src/PlaybackrateControl.tsx import { useCallback as useCallback6, useContext as useContext5, useEffect as useEffect9, useMemo as useMemo5, useState as useState8 } from "react"; import { Internals as Internals8 } from "remotion"; // src/utils/use-component-visible.ts import { useEffect as useEffect8, useRef as useRef6, useState as useState7 } from "react"; function useComponentVisible(initialIsVisible) { const [isComponentVisible, setIsComponentVisible] = useState7(initialIsVisible); const ref = useRef6(null); useEffect8(() => { const handleClickOutside = (event) => { if (ref.current && !ref.current.contains(event.target)) { setIsComponentVisible(false); } }; document.addEventListener("pointerup", handleClickOutside, true); return () => { document.removeEventListener("pointerup", handleClickOutside, true); }; }, []); return { ref, isComponentVisible, setIsComponentVisible }; } // src/PlaybackrateControl.tsx import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime"; var BOTTOM = 35; var THRESHOLD = 70; var rateDiv = { height: 30, paddingRight: 15, paddingLeft: 12, display: "flex", flexDirection: "row", alignItems: "center" }; var checkmarkContainer = { width: 22, display: "flex", alignItems: "center" }; var checkmarkStyle = { width: 14, height: 14, color: "black" }; var Checkmark = () => /* @__PURE__ */ jsx7("svg", { viewBox: "0 0 512 512", style: checkmarkStyle, children: /* @__PURE__ */ jsx7("path", { fill: "currentColor", d: "M435.848 83.466L172.804 346.51l-96.652-96.652c-4.686-4.686-12.284-4.686-16.971 0l-28.284 28.284c-4.686 4.686-4.686 12.284 0 16.971l133.421 133.421c4.686 4.686 12.284 4.686 16.971 0l299.813-299.813c4.686-4.686 4.686-12.284 0-16.971l-28.284-28.284c-4.686-4.686-12.284-4.686-16.97 0z" }) }); var PlaybackrateOption = ({ rate, onSelect, selectedRate, keyboardSelectedRate }) => { const onClick = useCallback6((e) => { e.stopPropagation(); e.preventDefault(); onSelect(rate); }, [onSelect, rate]); const [hovered, setHovered] = useState8(false); const onMouseEnter = useCallback6(() => { setHovered(true); }, []); const onMouseLeave = useCallback6(() => { setHovered(false); }, []); const isFocused = keyboardSelectedRate === rate; const actualStyle = useMemo5(() => { return { ...rateDiv, backgroundColor: hovered || isFocused ? "#eee" : "transparent" }; }, [hovered, isFocused]); return /* @__PURE__ */ jsxs5("div", { onMouseEnter, onMouseLeave, tabIndex: 0, style: actualStyle, onClick, children: [ /* @__PURE__ */ jsx7("div", { style: checkmarkContainer, children: rate === selectedRate ? /* @__PURE__ */ jsx7(Checkmark, {}) : null }), rate.toFixed(1), "x" ] }, rate); }; var PlaybackPopup = ({ setIsComponentVisible, playbackRates, canvasSize }) => { const { setPlaybackRate, playbackRate } = useContext5(Internals8.Timeline.TimelineContext); const [keyboardSelectedRate, setKeyboardSelectedRate] = useState8(playbackRate); useEffect9(() => { const listener = (e) => { e.preventDefault(); if (e.key === "ArrowUp") { const currentIndex = playbackRates.findIndex((rate) => rate === keyboardSelectedRate); if (currentIndex === 0) { return; } if (currentIndex === -1) { setKeyboardSelectedRate(playbackRates[0]); } else { setKeyboardSelectedRate(playbackRates[currentIndex - 1]); } } else if (e.key === "ArrowDown") { const currentIndex = playbackRates.findIndex((rate) => rate === keyboardSelectedRate); if (currentIndex === playbackRates.length - 1) { return; } if (currentIndex === -1) { setKeyboardSelectedRate(playbackRates[playbackRates.length - 1]); } else { setKeyboardSelectedRate(playbackRates[currentIndex + 1]); } } else if (e.key === "Enter") { setPlaybackRate(keyboardSelectedRate); setIsComponentVisible(false); } }; window.addEventListener("keydown", listener); return () => { window.removeEventListener("keydown", listener); }; }, [ playbackRates, keyboardSelectedRate, setPlaybackRate, setIsComponentVisible ]); const onSelect = useCallback6((rate) => { setPlaybackRate(rate); setIsComponentVisible(false); }, [setIsComponentVisible, setPlaybackRate]); const playbackPopup = useMemo5(() => { return { position: "absolute", right: 0, width: 125, maxHeight: canvasSize.height - THRESHOLD - BOTTOM, bottom: 35, background: "#fff", borderRadius: 4, overflow: "auto", color: "black", textAlign: "left" }; }, [canvasSize.height]); return /* @__PURE__ */ jsx7("div", { style: playbackPopup, children: playbackRates.map((rate) => { return /* @__PURE__ */ jsx7(PlaybackrateOption, { selectedRate: playbackRate, onSelect, rate, keyboardSelectedRate }, rate); }) }); }; var label = { fontSize: 13, fontWeight: "bold", color: "white", border: "2px solid white", borderRadius: 20, paddingLeft: 8, paddingRight: 8, paddingTop: 2, paddingBottom: 2 }; var playerButtonStyle = { appearance: "none", backgroundColor: "transparent", border: "none", cursor: "pointer", paddingLeft: 0, paddingRight: 0, paddingTop: 6, paddingBottom: 6, height: 37, display: "inline-flex", marginBottom: 0, marginTop: 0, alignItems: "center" }; var button = { ...playerButtonStyle, position: "relative" }; var PlaybackrateControl = ({ playbackRates, canvasSize }) => { const { ref, isComponentVisible, setIsComponentVisible } = useComponentVisible(false); const { playbackRate } = useContext5(Internals8.Timeline.TimelineContext); const onClick = useCallback6((e) => { e.stopPropagation(); e.preventDefault(); setIsComponentVisible((prevIsComponentVisible) => !prevIsComponentVisible); }, [setIsComponentVisible]); return /* @__PURE__ */ jsx7("div", { ref, children: /* @__PURE__ */ jsxs5("button", { type: "button", "aria-label": "Change playback rate", style: button, onClick, children: [ /* @__PURE__ */ jsxs5("div", { style: label, children: [ playbackRate, "x" ] }), isComponentVisible && /* @__PURE__ */ jsx7(PlaybackPopup, { canvasSize, playbackRates, setIsComponentVisible }) ] }) }); }; // src/PlayerSeekBar.tsx import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo6, useRef as useRef7, useState as useState9 } from "react"; import { Internals as Internals9, interpolate } from "remotion"; import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime"; var getFrameFromX = (clientX, durationInFrames, width) => { const pos = clientX; const frame = Math.round(interpolate(pos, [0, width], [0, durationInFrames - 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" })); return frame; }; var BAR_HEIGHT2 = 5; var KNOB_SIZE2 = 12; var VERTICAL_PADDING = 4; var containerStyle = { userSelect: "none", WebkitUserSelect: "none", paddingTop: VERTICAL_PADDING, paddingBottom: VERTICAL_PADDING, boxSizing: "border-box", cursor: "pointer", position: "relative", touchAction: "none" }; var barBackground = { height: BAR_HEIGHT2, backgroundColor: "rgba(255, 255, 255, 0.25)", width: "100%", borderRadius: BAR_HEIGHT2 / 2 }; var findBodyInWhichDivIsLocated = (div) => { let current = div; while (current.parentElement) { current = current.parentElement; } return current; }; var PlayerSeekBar = ({ durationInFrames, onSeekEnd, onSeekStart, inFrame, outFrame }) => { const containerRef = useRef7(null); const barHovered = useHoverState(containerRef, false); const size = useElementSize(containerRef, { triggerOnWindowResize: true, shouldApplyCssTransforms: true }); const { seek, play, pause, playing } = usePlayer(); const frame = Internals9.Timeline.useTimelinePosition(); const [dragging, setDragging] = useState9({ dragging: false }); const width = size?.width ?? 0; const onPointerDown = useCallback7((e) => { if (e.button !== 0) { return; } const posLeft = containerRef.current?.getBoundingClientRect().left; const _frame = getFrameFromX(e.clientX - posLeft, durationInFrames, width); pause(); seek(_frame); setDragging({ dragging: true, wasPlaying: playing }); onSeekStart(); }, [durationInFrames, width, pause, seek, playing, onSeekStart]); const onPointerMove = useCallback7((e) => { if (!size) { throw new Error("Player has no size"); } if (!dragging.dragging) { return; } const posLeft = containerRef.current?.getBoundingClientRect().left; const _frame = getFrameFromX(e.clientX - posLeft, durationInFrames, size.width); seek(_frame); }, [dragging.dragging, durationInFrames, seek, size]); const onPointerUp = useCallback7(() => { setDragging({ dragging: false }); if (!dragging.dragging) { return; } if (dragging.wasPlaying) { play(); } else { pause(); } onSeekEnd(); }, [dragging, onSeekEnd, pause, play]); useEffect10(() => { if (!dragging.dragging) { return; } const body = findBodyInWhichDivIsLocated(containerRef.current); body.addEventListener("pointermove", onPointerMove); body.addEventListener("pointerup", onPointerUp); return () => { bod