@vanillaskyai/video 0.10.6 → 0.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/{chapter-title-YRLIQY3V.js → chapter-title-2JVDU62E.js} +1 -1
- package/dist/check-runtime.js +2 -2
- package/dist/{chunk-EWWCQFTI.js → chunk-7AA2JWHZ.js} +3 -2
- package/dist/{chunk-DD4ZSYKG.js → chunk-J7EGPURK.js} +309 -129
- package/dist/{chunk-VWJBIYBM.js → chunk-KUCEOG2L.js} +53 -132
- package/dist/{chunk-XBIKTSFY.js → chunk-Q3LPOPHL.js} +1 -1
- package/dist/{chunk-JNZRVC45.js → chunk-R5ZX2LJV.js} +1 -1
- package/dist/{cinema-media-AYKCBXRI.js → cinema-media-WUS7CKPC.js} +3 -4
- package/dist/{comparison-74B2XEH4.js → comparison-T4OYCOH3.js} +3 -4
- package/dist/{editorial-timeline-DUW7C75H.js → editorial-timeline-JHQZIPNE.js} +3 -4
- package/dist/{key-figure-OEWIMPGS.js → key-figure-YWN2OBQU.js} +3 -4
- package/dist/{mobile-message-KBLF3WUR.js → mobile-message-CDKOERRZ.js} +3 -4
- package/dist/{quote-QCGR2HOU.js → quote-JKABOMIG.js} +3 -4
- package/dist/react.js +36 -22
- package/dist/server.js +15 -5
- package/package.json +1 -1
- package/registry/items/backgrounds.json +2 -2
- package/registry/items/chapterTitle.json +1 -1
- package/starters/video-chat/README.md +4 -8
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/server.ts +0 -5
- package/dist/chunk-MGCD3ZJ3.js +0 -207
- package/dist/scene-video-backdrop-MUGIXE2Q.js +0 -7
- package/dist/{chunk-OOBT4X46.js → chunk-5JBMYQP6.js} +34 -34
package/dist/server.js
CHANGED
|
@@ -895,14 +895,24 @@ var object = (value) => value && typeof value === "object" && !Array.isArray(val
|
|
|
895
895
|
function text(value, maximum) {
|
|
896
896
|
return typeof value === "string" && value.trim().length <= maximum ? value.trim() : "";
|
|
897
897
|
}
|
|
898
|
-
function
|
|
898
|
+
function chapterSubject(subject) {
|
|
899
|
+
const normalized = subject.replace(/\s+/gu, " ");
|
|
900
|
+
if (normalized.length <= 65) return normalized;
|
|
901
|
+
const prefix = normalized.slice(0, 65);
|
|
902
|
+
const boundary = prefix.lastIndexOf(" ");
|
|
903
|
+
return boundary > 0 ? prefix.slice(0, boundary) : "";
|
|
904
|
+
}
|
|
905
|
+
function readShot(value, clipDurationSec, answerSubject = "") {
|
|
899
906
|
const item = object(value);
|
|
900
907
|
const narration = text(item?.narration, 2e3);
|
|
901
908
|
if (!narration) throw new Error("Chat shot requires bounded authored narration");
|
|
909
|
+
const subject = text(item?.subject, 80);
|
|
910
|
+
const title = text(item?.title, 65) || chapterSubject(subject) || chapterSubject(answerSubject);
|
|
911
|
+
if (!title) throw new Error("Chat shot requires an authored chapter title or subject");
|
|
902
912
|
return {
|
|
903
913
|
narration,
|
|
904
|
-
title
|
|
905
|
-
subject
|
|
914
|
+
title,
|
|
915
|
+
subject,
|
|
906
916
|
action: text(item?.action, 600),
|
|
907
917
|
durationSec: typeof item?.durationSec === "number" && Number.isFinite(item.durationSec) ? Math.min(clipDurationSec, Math.max(2, item.durationSec)) : clipDurationSec,
|
|
908
918
|
continuity: item?.continuity === "continue" ? "continue" : "cut"
|
|
@@ -973,7 +983,7 @@ function createChatShotPlanner(options) {
|
|
|
973
983
|
brief = { opening: text(part.opening, 300), subject: text(part.subject, 80), visualDirection: text(part.visualDirection, 600), development: text(part.development, 2e3) };
|
|
974
984
|
if (part.ending) {
|
|
975
985
|
try {
|
|
976
|
-
brief.ending = readShot(part.ending, clipDurationSec);
|
|
986
|
+
brief.ending = readShot(part.ending, clipDurationSec, brief.subject);
|
|
977
987
|
} catch (cause) {
|
|
978
988
|
reject(cause);
|
|
979
989
|
}
|
|
@@ -983,7 +993,7 @@ function createChatShotPlanner(options) {
|
|
|
983
993
|
}
|
|
984
994
|
if (part?.type !== "shot") throw new Error("Chat plan requires an answer brief followed by shots");
|
|
985
995
|
if (!brief) throw new Error("Chat shot arrived before its answer brief");
|
|
986
|
-
const shot = readShot(part, clipDurationSec);
|
|
996
|
+
const shot = readShot(part, clipDurationSec, brief.subject);
|
|
987
997
|
if (shot.narration === brief.ending?.narration) return;
|
|
988
998
|
if (firstBody && !continueAfterOpening(shot.narration, [options.openingLine ?? brief.opening])) return;
|
|
989
999
|
const budget = (context.request.input.maxDurationSec ?? 40) - (brief.ending?.durationSec ?? clipDurationSec);
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"path": "src/visual-system/scene-templates/external-video-backdrop.tsx",
|
|
29
29
|
"type": "registry:lib",
|
|
30
30
|
"target": "vanillasky/scene-templates/external-video-backdrop.tsx",
|
|
31
|
-
"content": "import React from \"react\";\n\n// Source-owned templates may live in a consumer's tree while VideoFrame comes\n// from the package. Both copies must observe the same internal context or the\n// consumer template would mount a second video over the player-owned plane,\n// and would never inherit the player's native media audio state.\nexport type ExternalVideoBackdropMode = false | \"pending\" | \"ready\" | \"fallback\";\n\ninterface BackdropContextValue {\n mode: ExternalVideoBackdropMode;\n audioMuted: boolean;\n audioVolume: number;\n preparingNarration?: boolean;\n onMediaError?: () => void;\n}\n\nconst DEFAULT: BackdropContextValue = { mode: false, audioMuted: true, audioVolume: 1 };\n\nconst sharedContext = globalThis as typeof globalThis & {\n __vanillaskyVideoBackdropContext?: React.Context<BackdropContextValue>;\n};\nconst BackdropContext = sharedContext.__vanillaskyVideoBackdropContext\n ??= React.createContext<BackdropContextValue>(DEFAULT);\n\nexport function ExternalVideoBackdropProvider({\n mode,\n audioMuted = true,\n audioVolume = 1,\n preparingNarration = false,\n onMediaError,\n children,\n}: {\n mode: ExternalVideoBackdropMode;\n audioMuted?: boolean;\n audioVolume?: number;\n preparingNarration?: boolean;\n onMediaError?: () => void;\n children: React.ReactNode;\n}) {\n const value = React.useMemo(\n () => ({ mode, audioMuted, audioVolume, preparingNarration, onMediaError }),\n [mode, audioMuted, audioVolume, preparingNarration, onMediaError],\n );\n return (\n <BackdropContext.Provider value={value}>\n {children}\n </BackdropContext.Provider>\n );\n}\n\nexport function useExternalVideoBackdrop(): ExternalVideoBackdropMode {\n return React.useContext(BackdropContext).mode;\n}\n\nexport function useMediaAudio(): { muted: boolean; volume: number } {\n const { audioMuted, audioVolume } = React.useContext(BackdropContext);\n return { muted: audioMuted, volume: audioVolume };\n}\n\n/** Internal first-frame priming state; an explicit viewer pause never sets it. */\nexport function useNarrationPreroll(): boolean {\n return React.useContext(BackdropContext).preparingNarration === true;\n}\n\n/** Routes local decoder/playback failures to the scene-owned recovery surface. */\nexport function useMediaFailure(): (() => void) | undefined {\n return React.useContext(BackdropContext).onMediaError;\n}\n"
|
|
31
|
+
"content": "import React from \"react\";\n\n// Source-owned templates may live in a consumer's tree while VideoFrame comes\n// from the package. Both copies must observe the same internal context or the\n// consumer template would mount a second video over the player-owned plane,\n// and would never inherit the player's native media audio state.\nexport type ExternalVideoBackdropMode = false | \"pending\" | \"ready\" | \"fallback\";\n\nexport type MediaRecoveryReason = \"decode-error\" | \"frame-readiness-timeout\" | \"stalled-media\" | \"playback-error\";\n\ninterface BackdropContextValue {\n mode: ExternalVideoBackdropMode;\n audioMuted: boolean;\n audioVolume: number;\n preparingNarration?: boolean;\n onMediaError?: (reason?: MediaRecoveryReason) => void;\n}\n\nconst DEFAULT: BackdropContextValue = { mode: false, audioMuted: true, audioVolume: 1 };\n\nconst sharedContext = globalThis as typeof globalThis & {\n __vanillaskyVideoBackdropContext?: React.Context<BackdropContextValue>;\n};\nconst BackdropContext = sharedContext.__vanillaskyVideoBackdropContext\n ??= React.createContext<BackdropContextValue>(DEFAULT);\n\nexport function ExternalVideoBackdropProvider({\n mode,\n audioMuted = true,\n audioVolume = 1,\n preparingNarration = false,\n onMediaError,\n children,\n}: {\n mode: ExternalVideoBackdropMode;\n audioMuted?: boolean;\n audioVolume?: number;\n preparingNarration?: boolean;\n onMediaError?: (reason?: MediaRecoveryReason) => void;\n children: React.ReactNode;\n}) {\n const value = React.useMemo(\n () => ({ mode, audioMuted, audioVolume, preparingNarration, onMediaError }),\n [mode, audioMuted, audioVolume, preparingNarration, onMediaError],\n );\n return (\n <BackdropContext.Provider value={value}>\n {children}\n </BackdropContext.Provider>\n );\n}\n\nexport function useExternalVideoBackdrop(): ExternalVideoBackdropMode {\n return React.useContext(BackdropContext).mode;\n}\n\nexport function useMediaAudio(): { muted: boolean; volume: number } {\n const { audioMuted, audioVolume } = React.useContext(BackdropContext);\n return { muted: audioMuted, volume: audioVolume };\n}\n\n/** Internal first-frame priming state; an explicit viewer pause never sets it. */\nexport function useNarrationPreroll(): boolean {\n return React.useContext(BackdropContext).preparingNarration === true;\n}\n\n/** Routes local decoder/playback failures to the scene-owned recovery surface. */\nexport function useMediaFailure(): ((reason?: MediaRecoveryReason) => void) | undefined {\n return React.useContext(BackdropContext).onMediaError;\n}\n"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"path": "src/visual-system/scene-templates/media-position.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"path": "src/visual-system/scene-templates/scene-video-backdrop.tsx",
|
|
41
41
|
"type": "registry:lib",
|
|
42
42
|
"target": "vanillasky/scene-templates/scene-video-backdrop.tsx",
|
|
43
|
-
"content": "import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { getBackgroundTransform } from \"../backgrounds\";\nimport { useMediaAudio, useMediaFailure, useNarrationPreroll } from \"./external-video-backdrop\";\nimport { resolveMediaPosition } from \"./media-position\";\n\nexport interface SceneVideoBackdropProps {\n mediaUrl: string;\n mediaPoster?: string;\n mediaPosition?: string;\n backgroundEffect?: string;\n progress: number;\n /** Narration-led visible duration; muted or pitch-preserving footage may be gently retimed. */\n sceneDuration?: number;\n /** Internal player-owned decoder priming, distinct from viewer pause. */\n preparingNarration?: boolean;\n beatIntensity?: number;\n isPlaying: boolean;\n muted?: boolean;\n volume?: number;\n playbackId?: string;\n retainPoster?: boolean;\n persistent?: boolean;\n preparedPoster?: {\n presentationKey: string;\n mediaPoster: string;\n mediaPosition: string;\n backgroundEffect?: string;\n /** Existing global transition progress. On decoder-constrained Safari,\n * this fades the decoded incoming still above the outgoing video before\n * the single video element changes source. */\n opacity?: number;\n };\n onReady?: () => void;\n onError?: () => void;\n}\n\nexport const SceneVideoBackdrop: React.FC<SceneVideoBackdropProps> = ({\n mediaUrl,\n mediaPoster,\n mediaPosition = \"center\",\n backgroundEffect,\n progress,\n sceneDuration,\n preparingNarration = false,\n beatIntensity = 0,\n isPlaying,\n muted,\n volume,\n playbackId = mediaUrl,\n retainPoster = false,\n persistent = false,\n preparedPoster,\n onReady,\n onError,\n}) => {\n const inheritedAudio = useMediaAudio();\n const reportMediaFailure = useMediaFailure();\n const inheritedPreroll = useNarrationPreroll();\n const rewindPreroll = preparingNarration || inheritedPreroll;\n const resolvedMuted = muted ?? inheritedAudio.muted;\n const resolvedVolume = volume ?? inheritedAudio.volume;\n const resolvedPosition = resolveMediaPosition(mediaPosition);\n const bgTransform = getBackgroundTransform(backgroundEffect, progress, beatIntensity);\n const [decodedVideoUrl, setDecodedVideoUrl] = useState<string>();\n const [waitingKey, setWaitingKey] = useState<string>();\n const [exhaustedKey, setExhaustedKey] = useState<string>();\n\n const videoRef = useRef<HTMLVideoElement>(null);\n const startedVideoUrl = useRef<string | undefined>(undefined);\n const startedPlaybackId = useRef<string | undefined>(undefined);\n const videoPresentationKey = `${playbackId}\\0${mediaUrl}`;\n\n const presentationRef = useRef({ key: videoPresentationKey, playing: isPlaying });\n presentationRef.current = { key: videoPresentationKey, playing: isPlaying };\n const unavailable = () => {\n if (presentationRef.current.key === videoPresentationKey && presentationRef.current.playing) {\n setExhaustedKey(videoPresentationKey);\n onError?.();\n reportMediaFailure?.();\n }\n };\n useEffect(() => {\n if (!isPlaying || waitingKey !== videoPresentationKey) {\n if (waitingKey) setWaitingKey(undefined);\n return;\n }\n const video = videoRef.current;\n if (!video) return;\n let previousTime = video.currentTime;\n let forwardFrames = 0;\n let stopped = false;\n let frame: number | undefined;\n let poll: ReturnType<typeof setTimeout> | undefined;\n const observe = (_now?: number, metadata?: VideoFrameCallbackMetadata) => {\n if (stopped) return;\n const time = metadata?.mediaTime ?? video.currentTime;\n if (video.seeking || time < previousTime) forwardFrames = 0;\n else if (time > previousTime + .001) forwardFrames++;\n previousTime = time;\n // One seek frame is not resumed motion. Require consecutive forward\n // observations before releasing the original bounded stall deadline.\n if (forwardFrames >= 2) {\n stopped = true;\n clearTimeout(deadline);\n setWaitingKey(undefined);\n return;\n }\n if (video.requestVideoFrameCallback) frame = video.requestVideoFrameCallback(observe);\n else poll = setTimeout(observe, 50);\n };\n // A seek can emit waiting without another playing event, even while frames\n // resume. Keep the decoder visible and observe motion directly. A real\n // stall gets the player's authored chapter instead of an endless spinner.\n const deadline = setTimeout(() => {\n if (!stopped) { stopped = true; unavailable(); }\n }, 1000);\n observe();\n return () => {\n stopped = true;\n clearTimeout(deadline);\n clearTimeout(poll);\n if (frame !== undefined) video.cancelVideoFrameCallback?.(frame);\n };\n }, [waitingKey, videoPresentationKey, isPlaying]);\n\n const fitDuration = useCallback((video: HTMLVideoElement) => {\n // Allow a small decode-to-speech onset margin without changing narration.\n video.playbackRate = (resolvedMuted || video.preservesPitch === true) && sceneDuration && Number.isFinite(video.duration) && video.duration > 0\n ? Math.max(.75, Math.min(1, video.duration / (sceneDuration + .2))) : 1;\n }, [resolvedMuted, sceneDuration]);\n useEffect(() => {\n if (videoRef.current) fitDuration(videoRef.current);\n }, [fitDuration]);\n const continueMotion = (video: HTMLVideoElement) => {\n if (!isPlaying) return;\n // The finite scene clock bounds silent coverage. Speech may outlast a\n // short clip; repeat motion until the scene ends, never audible dialogue.\n if (!resolvedMuted) {\n unavailable();\n return;\n }\n video.currentTime = 0;\n void video.play().catch(unavailable);\n };\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n // React Strict Mode rehearses setup → cleanup → setup in development.\n // The cleanup deliberately releases the decoder, so the repeated setup\n // must restore the declarative source before the playback effect runs.\n if (video.getAttribute(\"src\") !== mediaUrl) {\n video.setAttribute(\"src\", mediaUrl);\n video.load();\n }\n return () => {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n startedVideoUrl.current = undefined;\n startedPlaybackId.current = undefined;\n };\n }, [mediaUrl]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (video) video.volume = resolvedVolume;\n }, [resolvedVolume]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n if (!isPlaying) {\n video.pause();\n if (rewindPreroll && video.currentTime > 0) video.currentTime = 0;\n return;\n }\n if (startedPlaybackId.current === playbackId) {\n if (video.ended) continueMotion(video);\n else void video.play().catch(unavailable);\n return;\n }\n const changingSource = startedVideoUrl.current !== undefined && startedVideoUrl.current !== mediaUrl;\n fitDuration(video);\n if (!changingSource && video.currentTime > 0) video.currentTime = 0;\n video.play().catch(unavailable);\n startedVideoUrl.current = mediaUrl;\n startedPlaybackId.current = playbackId;\n }, [isPlaying, mediaUrl, playbackId, rewindPreroll]);\n\n const mediaStyle: React.CSSProperties = {\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n objectPosition: resolvedPosition,\n transform: bgTransform.transform,\n transformOrigin: bgTransform.transformOrigin,\n zIndex: persistent ? 1 : undefined,\n };\n const preparedPosition = preparedPoster\n ? resolveMediaPosition(preparedPoster.mediaPosition)\n : resolvedPosition;\n const preparedTransform = getBackgroundTransform(preparedPoster?.backgroundEffect, 0, 0);\n const posterPlanes = [\n ...(persistent && mediaPoster ? [{\n presentationKey: videoPresentationKey,\n mediaPoster,\n mediaPosition: resolvedPosition,\n transform: bgTransform.transform,\n transformOrigin: bgTransform.transformOrigin,\n opacity: 1,\n zIndex: 0,\n role: \"current\",\n }] : []),\n ...(preparedPoster && preparedPoster.presentationKey !== videoPresentationKey ? [{\n presentationKey: preparedPoster.presentationKey,\n mediaPoster: preparedPoster.mediaPoster,\n mediaPosition: preparedPosition,\n transform: preparedTransform.transform,\n transformOrigin: preparedTransform.transformOrigin,\n opacity: preparedPoster.opacity ?? 0,\n zIndex: 2,\n role: \"prepared\",\n }] : []),\n ];\n\n return (\n <>\n {posterPlanes.map((posterPlane) => (\n <img\n key={posterPlane.presentationKey}\n src={posterPlane.mediaPoster}\n alt=\"\"\n aria-hidden=\"true\"\n draggable={false}\n data-video-poster-plane={posterPlane.role}\n data-video-poster-visible={posterPlane.opacity > 0 ? \"true\" : \"false\"}\n style={{\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n objectPosition: posterPlane.mediaPosition,\n transform: posterPlane.transform,\n transformOrigin: posterPlane.transformOrigin,\n zIndex: posterPlane.zIndex,\n opacity: posterPlane.opacity,\n pointerEvents: \"none\",\n }}\n />\n ))}\n {exhaustedKey === videoPresentationKey && <div\n role=\"status\" data-media-continuity=\"exhausted\"\n style={{ position: \"absolute\", inset: 0, zIndex: 3, background: \"#000\", color: \"#bbb\", display: \"grid\", placeContent: \"center\", font: \"14px system-ui\" }}\n >Visual unavailable</div>}\n <video\n ref={videoRef}\n src={mediaUrl}\n poster={retainPoster || decodedVideoUrl !== mediaUrl ? mediaPoster || undefined : undefined}\n muted={resolvedMuted}\n loop={false}\n playsInline\n preload=\"auto\"\n onLoadedMetadata={event => fitDuration(event.currentTarget)}\n onEnded={event => continueMotion(event.currentTarget)}\n onWaiting={() => { if (isPlaying) setWaitingKey(videoPresentationKey); }}\n onLoadedData={(event) => {\n const video = event.currentTarget;\n const markPresented = () => {\n if (!video.isConnected) return;\n onReady?.();\n if (!retainPoster) setDecodedVideoUrl(mediaUrl);\n };\n if (video.requestVideoFrameCallback) {\n video.requestVideoFrameCallback(markPresented);\n return;\n }\n markPresented();\n }}\n onError={onError}\n data-media-position={mediaPosition}\n data-video-backdrop={persistent ? \"persistent\" : \"scene\"}\n style={{ ...mediaStyle, visibility: exhaustedKey === videoPresentationKey ? \"hidden\" : undefined }}\n />\n </>\n );\n};\n"
|
|
43
|
+
"content": "import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { getBackgroundTransform } from \"../backgrounds\";\nimport { type MediaRecoveryReason, useMediaAudio, useMediaFailure, useNarrationPreroll } from \"./external-video-backdrop\";\nimport { resolveMediaPosition } from \"./media-position\";\n\nexport interface SceneVideoBackdropProps {\n mediaUrl: string;\n mediaPoster?: string;\n mediaPosition?: string;\n backgroundEffect?: string;\n progress: number;\n /** Narration-led visible duration; muted or pitch-preserving footage may be gently retimed. */\n sceneDuration?: number;\n /** Internal player-owned decoder priming, distinct from viewer pause. */\n preparingNarration?: boolean;\n beatIntensity?: number;\n isPlaying: boolean;\n muted?: boolean;\n volume?: number;\n playbackId?: string;\n onReady?: () => void;\n onError?: () => void;\n}\n\nexport const SceneVideoBackdrop: React.FC<SceneVideoBackdropProps> = ({\n mediaUrl,\n mediaPoster,\n mediaPosition = \"center\",\n backgroundEffect,\n progress,\n sceneDuration,\n preparingNarration = false,\n beatIntensity = 0,\n isPlaying,\n muted,\n volume,\n playbackId = mediaUrl,\n onReady,\n onError,\n}) => {\n const inheritedAudio = useMediaAudio();\n const reportMediaFailure = useMediaFailure();\n const inheritedPreroll = useNarrationPreroll();\n const rewindPreroll = preparingNarration || inheritedPreroll;\n const resolvedMuted = muted ?? inheritedAudio.muted;\n const resolvedVolume = volume ?? inheritedAudio.volume;\n const resolvedPosition = resolveMediaPosition(mediaPosition);\n const bgTransform = getBackgroundTransform(backgroundEffect, progress, beatIntensity);\n const [decodedVideoUrl, setDecodedVideoUrl] = useState<string>();\n const [waitingKey, setWaitingKey] = useState<string>();\n const [exhaustedKey, setExhaustedKey] = useState<string>();\n\n const videoRef = useRef<HTMLVideoElement>(null);\n const presentedVideoUrl = useRef<string | undefined>(undefined);\n const startedVideoUrl = useRef<string | undefined>(undefined);\n const startedPlaybackId = useRef<string | undefined>(undefined);\n const videoPresentationKey = `${playbackId}\\0${mediaUrl}`;\n\n const presentationRef = useRef({ key: videoPresentationKey, playing: isPlaying });\n presentationRef.current = { key: videoPresentationKey, playing: isPlaying };\n const unavailable = (reason: MediaRecoveryReason = \"playback-error\") => {\n if (presentationRef.current.key === videoPresentationKey && presentationRef.current.playing) {\n setExhaustedKey(videoPresentationKey);\n onError?.();\n reportMediaFailure?.(reason);\n }\n };\n useEffect(() => {\n if (!isPlaying || waitingKey !== videoPresentationKey) {\n if (waitingKey) setWaitingKey(undefined);\n return;\n }\n const video = videoRef.current;\n if (!video) return;\n const expectedSource = video.getAttribute(\"src\") === mediaUrl ? video.src : undefined;\n let awaitingFirstFrame = presentedVideoUrl.current !== mediaUrl || video.currentSrc !== expectedSource;\n let previousTime = video.currentTime;\n let forwardFrames = 0;\n let stopped = false;\n let frame: number | undefined;\n let poll: ReturnType<typeof setTimeout> | undefined;\n const observe = (_now?: number, metadata?: VideoFrameCallbackMetadata) => {\n if (stopped) return;\n const currentSource = video.currentSrc === expectedSource;\n if (currentSource && awaitingFirstFrame && (metadata || presentedVideoUrl.current === mediaUrl)) {\n awaitingFirstFrame = false;\n clearTimeout(deadline);\n deadline = setTimeout(fail, 1000);\n }\n const time = metadata?.mediaTime ?? video.currentTime;\n if (!currentSource || video.seeking || time < previousTime) forwardFrames = 0;\n else if (time > previousTime + .001) forwardFrames++;\n previousTime = time;\n // One seek frame is not resumed motion. Require consecutive forward\n // observations before releasing the original bounded stall deadline.\n if (forwardFrames >= 2) {\n stopped = true;\n clearTimeout(deadline);\n setWaitingKey(undefined);\n return;\n }\n if (video.requestVideoFrameCallback) frame = video.requestVideoFrameCallback(observe);\n else poll = setTimeout(observe, 50);\n };\n // A seek can emit waiting without another playing event, even while frames\n // resume. Keep the decoder visible and observe motion directly. A real\n // stall gets the player's authored chapter instead of an endless spinner.\n const fail = () => { if (!stopped) { stopped = true; unavailable(awaitingFirstFrame ? \"frame-readiness-timeout\" : \"stalled-media\"); } };\n // Initial network/decode work has the same bound as mounted readiness.\n // Only a source that has presented a frame can be judged as stalled motion.\n let deadline = setTimeout(fail, awaitingFirstFrame ? 8000 : 1000);\n observe();\n return () => {\n stopped = true;\n clearTimeout(deadline);\n clearTimeout(poll);\n if (frame !== undefined) video.cancelVideoFrameCallback?.(frame);\n };\n }, [waitingKey, videoPresentationKey, isPlaying]);\n\n const onReadyRef = useRef(onReady);\n onReadyRef.current = onReady;\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n let stopped = false;\n let frame: number | undefined;\n const markPresented = () => {\n if (stopped || !video.isConnected || presentationRef.current.key !== videoPresentationKey\n || video.getAttribute(\"src\") !== mediaUrl || video.currentSrc !== video.src) return false;\n presentedVideoUrl.current = mediaUrl;\n video.dispatchEvent(new Event(\"vanillasky:video-frame-presented\", { bubbles: true }));\n onReadyRef.current?.();\n setDecodedVideoUrl(mediaUrl);\n stopped = true;\n return true;\n };\n const observe = () => {\n if (stopped || frame !== undefined) return;\n if (video.requestVideoFrameCallback) {\n frame = video.requestVideoFrameCallback(() => {\n frame = undefined;\n if (!markPresented()) observe();\n });\n } else if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) markPresented();\n };\n // A cached resource can finish loading while its Suspense tree is still\n // detached. Observe the mounted frame even if loadeddata was missed.\n video.addEventListener(\"loadeddata\", observe);\n observe();\n return () => {\n stopped = true;\n video.removeEventListener(\"loadeddata\", observe);\n if (frame !== undefined) video.cancelVideoFrameCallback?.(frame);\n };\n }, [mediaUrl, videoPresentationKey]);\n\n const fitDuration = useCallback((video: HTMLVideoElement) => {\n // Allow a small decode-to-speech onset margin without changing narration.\n video.playbackRate = (resolvedMuted || video.preservesPitch === true) && sceneDuration && Number.isFinite(video.duration) && video.duration > 0\n ? Math.max(.75, Math.min(1, video.duration / (sceneDuration + .2))) : 1;\n }, [resolvedMuted, sceneDuration]);\n useEffect(() => {\n if (videoRef.current) fitDuration(videoRef.current);\n }, [fitDuration]);\n const continueMotion = (video: HTMLVideoElement) => {\n if (!isPlaying) return;\n // The finite scene clock bounds silent coverage. Speech may outlast a\n // short clip; repeat motion until the scene ends, never audible dialogue.\n if (!resolvedMuted) {\n unavailable();\n return;\n }\n video.currentTime = 0;\n void video.play().catch(() => unavailable());\n };\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n // React Strict Mode rehearses setup → cleanup → setup in development.\n // The cleanup deliberately releases the decoder, so the repeated setup\n // must restore the declarative source before the playback effect runs.\n if (video.getAttribute(\"src\") !== mediaUrl) {\n video.setAttribute(\"src\", mediaUrl);\n video.load();\n }\n }, [mediaUrl]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n // A source change already starts a native load via React's src update.\n // Tear down the decoder only on unmount, never cancel that new request.\n return () => {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n startedVideoUrl.current = undefined;\n startedPlaybackId.current = undefined;\n };\n }, []);\n\n useEffect(() => {\n const video = videoRef.current;\n if (video) video.volume = resolvedVolume;\n }, [resolvedVolume]);\n\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n if (!isPlaying) {\n video.pause();\n if (rewindPreroll && video.currentTime > 0) video.currentTime = 0;\n return;\n }\n if (startedPlaybackId.current === playbackId) {\n if (video.ended) continueMotion(video);\n else void video.play().catch(() => unavailable());\n return;\n }\n const changingSource = startedVideoUrl.current !== undefined && startedVideoUrl.current !== mediaUrl;\n fitDuration(video);\n if (!changingSource && video.currentTime > 0) video.currentTime = 0;\n video.play().catch(() => unavailable());\n startedVideoUrl.current = mediaUrl;\n startedPlaybackId.current = playbackId;\n }, [isPlaying, mediaUrl, playbackId, rewindPreroll]);\n\n const enforceRequestedPause = (event: React.SyntheticEvent<HTMLVideoElement>) => {\n // WebKit may enter playback without a playing event while seeking or\n // waiting. Both native start events must honor the latest requested hold.\n if (presentationRef.current.playing) return;\n event.currentTarget.pause();\n if (rewindPreroll && event.currentTarget.currentTime > 0) event.currentTarget.currentTime = 0;\n };\n\n const mediaStyle: React.CSSProperties = {\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n objectPosition: resolvedPosition,\n transform: bgTransform.transform,\n transformOrigin: bgTransform.transformOrigin,\n };\n return (\n <>\n {exhaustedKey === videoPresentationKey && <div\n role=\"status\" data-media-continuity=\"exhausted\"\n style={{ position: \"absolute\", inset: 0, zIndex: 3, background: \"#000\", color: \"#bbb\", display: \"grid\", placeContent: \"center\", font: \"14px system-ui\" }}\n >Visual unavailable</div>}\n <video\n ref={videoRef}\n src={mediaUrl}\n poster={decodedVideoUrl !== mediaUrl ? mediaPoster || undefined : undefined}\n muted={resolvedMuted}\n loop={false}\n playsInline\n preload=\"auto\"\n onLoadedMetadata={event => fitDuration(event.currentTarget)}\n onEnded={event => continueMotion(event.currentTarget)}\n onPlay={enforceRequestedPause}\n onPlaying={enforceRequestedPause}\n onWaiting={() => { if (isPlaying) setWaitingKey(videoPresentationKey); }}\n onError={onError}\n data-media-position={mediaPosition}\n data-video-backdrop=\"scene\"\n style={{ ...mediaStyle, visibility: exhaustedKey === videoPresentationKey ? \"hidden\" : undefined }}\n />\n </>\n );\n};\n"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"path": "src/visual-system/scene-templates/color-utils.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"path": "src/visual-system/scene-templates/chapter-title.tsx",
|
|
17
17
|
"type": "registry:component",
|
|
18
18
|
"target": "vanillasky/scene-templates/chapter-title.tsx",
|
|
19
|
-
"content": "import type { SceneTemplateProps } from './types';\n\nimport {editorialFont,fade as ease} from './editorial-typography';\n\n/** A quiet chapter beat: fade in, read, fade to black. */\nfunction TitleScene({ variables, width, height, progress }: SceneTemplateProps) {\n const title = String(variables.title ?? 'A different perspective');\n const unit = Math.min(width, height);\n const opacity = ease(
|
|
19
|
+
"content": "import type { SceneTemplateProps } from './types';\n\nimport {editorialFont,fade as ease} from './editorial-typography';\n\n/** A quiet chapter beat: fade in, read, fade to black. */\nfunction TitleScene({ variables, width, height, progress, motionProgress }: SceneTemplateProps) {\n const title = String(variables.title ?? 'A different perspective');\n const unit = Math.min(width, height);\n const presentation = motionProgress ?? progress;\n const opacity = ease(presentation / .22) * (1 - ease((presentation - .76) / .24));\n return <div data-template=\"title\" data-title-treatment=\"quiet-fade\" style={{position:'absolute',inset:0,background:'#000',color:'#fff',display:'flex',alignItems:'center',justifyContent:'center'}}>\n <div data-title-composition=\"centered\" style={{width:'76%',textAlign:'center',opacity,fontFamily:editorialFont,fontWeight:500,fontSize:unit * .068,lineHeight:1.18,letterSpacing:'-.025em',textWrap:'balance',overflowWrap:'anywhere'}}>{title}</div>\n </div>;\n}\n\nexport const TitleSceneTemplate = TitleScene;\n"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "src/visual-system/scene-templates/types.ts",
|
|
@@ -49,15 +49,11 @@ call. A template introduction starts while footage prepares; body shots contain
|
|
|
49
49
|
moving footage, narration and subtitles. Intent changes the visible actions and
|
|
50
50
|
pacing, not the rendering pipeline.
|
|
51
51
|
|
|
52
|
-
The SDK tries generated footage first, then relevant approved stock when the
|
|
53
|
-
provider is unavailable, denied, or fails. Attempts, including failures, share
|
|
54
|
-
the host's `maxGeneratedVideos` ceiling. A stock miss never broadens the subject
|
|
55
|
-
automatically. If no relevant footage is available, narration and subtitles
|
|
56
|
-
continue as authored chapter scenes.
|
|
57
|
-
|
|
58
52
|
Choose AI video or Pexels in Settings. Each mode uses only its selected footage
|
|
59
|
-
provider, and both use chapter scenes when footage cannot be prepared.
|
|
60
|
-
|
|
53
|
+
provider, and both use chapter scenes when footage cannot be prepared. AI video
|
|
54
|
+
attempts, including failures, share the host's `maxGeneratedVideos` ceiling.
|
|
55
|
+
Exhausted allowance recovers directly to authored chapters without stock calls.
|
|
56
|
+
The Pexels adapter searches the full catalog with bounded subject matching,
|
|
61
57
|
orientation selection and caching; it no longer requires a reviewed index.
|
|
62
58
|
Custom interfaces must display a prominent [Pexels](https://www.pexels.com) credit.
|
|
63
59
|
|
|
@@ -25,11 +25,6 @@ export const handleVideoChat = createVideoChatHandler({
|
|
|
25
25
|
output_config: { effort: "medium" },
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
|
-
onFinish: ({ finishReason, text }) => {
|
|
29
|
-
if (finishReason !== "stop") console.warn(`[planner] ${finishReason}: ${text.slice(0, 300)}`);
|
|
30
|
-
const keyed = (text.match(/"mediaKeyword"/g) ?? []).length;
|
|
31
|
-
console.log(`[planner] ${(text.match(/"scene\.add"/g) ?? []).length} scenes, ${keyed} asked for footage`);
|
|
32
|
-
},
|
|
33
28
|
}),
|
|
34
29
|
generateText: async ({ systemPrompt, userPrompt, maxOutputTokens, signal }) => {
|
|
35
30
|
const { text } = await generateText({
|
package/dist/chunk-MGCD3ZJ3.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BrandGradientOverlay,
|
|
3
|
-
SceneVideoBackdrop,
|
|
4
|
-
getBackgroundTransform,
|
|
5
|
-
resolveMediaPosition
|
|
6
|
-
} from "./chunk-DD4ZSYKG.js";
|
|
7
|
-
import {
|
|
8
|
-
resolveMediaType
|
|
9
|
-
} from "./chunk-224QNWRA.js";
|
|
10
|
-
import {
|
|
11
|
-
useExternalVideoBackdrop
|
|
12
|
-
} from "./chunk-OOBT4X46.js";
|
|
13
|
-
|
|
14
|
-
// src/visual-system/scene-templates/scene-background.tsx
|
|
15
|
-
import { useEffect, useState } from "react";
|
|
16
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
-
function resolveMediaTreatment(value) {
|
|
18
|
-
return value === "none" || value === "subtle" || value === "text-safe" ? value : "cinematic";
|
|
19
|
-
}
|
|
20
|
-
var SCRIM_STOP_COUNT = 7;
|
|
21
|
-
function smoothstep(t) {
|
|
22
|
-
return t * t * (3 - 2 * t);
|
|
23
|
-
}
|
|
24
|
-
function easedStops(peakAlpha, start, end, direction) {
|
|
25
|
-
const alphaAt = (t) => {
|
|
26
|
-
const eased = direction === "fade-out" ? 1 - smoothstep(t) : smoothstep(t);
|
|
27
|
-
return `rgba(0,0,0,${Number((peakAlpha * eased).toFixed(3))})`;
|
|
28
|
-
};
|
|
29
|
-
const stops = [];
|
|
30
|
-
if (start > 0) stops.push(`${alphaAt(0)} 0%`);
|
|
31
|
-
for (let i = 0; i < SCRIM_STOP_COUNT; i += 1) {
|
|
32
|
-
const t = i / (SCRIM_STOP_COUNT - 1);
|
|
33
|
-
const position = Number((start + (end - start) * t).toFixed(2));
|
|
34
|
-
stops.push(`${alphaAt(t)} ${position}%`);
|
|
35
|
-
}
|
|
36
|
-
if (end < 100) stops.push(`${alphaAt(1)} 100%`);
|
|
37
|
-
return stops.join(", ");
|
|
38
|
-
}
|
|
39
|
-
function getMediaTreatmentLayers(value, anchor = "full") {
|
|
40
|
-
const treatment = resolveMediaTreatment(value);
|
|
41
|
-
if (treatment === "none") return [];
|
|
42
|
-
const vignette = {
|
|
43
|
-
id: "vignette",
|
|
44
|
-
background: treatment === "subtle" ? `radial-gradient(ellipse at center, ${easedStops(0.28, 45, 100, "fade-in")})` : `radial-gradient(ellipse at center, ${easedStops(0.72, 32, 100, "fade-in")})`
|
|
45
|
-
};
|
|
46
|
-
if (treatment === "subtle") return [vignette];
|
|
47
|
-
const textSafe = treatment === "text-safe";
|
|
48
|
-
const layers = [vignette];
|
|
49
|
-
if (anchor !== "bottom") {
|
|
50
|
-
layers.push({
|
|
51
|
-
id: "center-scrim",
|
|
52
|
-
background: textSafe ? `radial-gradient(ellipse 92% 58% at 50% 50%, ${easedStops(0.46, 34, 90, "fade-out")})` : `radial-gradient(ellipse 88% 52% at 50% 50%, ${easedStops(0.26, 30, 88, "fade-out")})`
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
if (anchor !== "center") {
|
|
56
|
-
layers.push({
|
|
57
|
-
id: "bottom-scrim",
|
|
58
|
-
background: `linear-gradient(to top, ${easedStops(textSafe ? 0.64 : 0.5, 8, 100, "fade-out")})`,
|
|
59
|
-
style: { top: "55%" }
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
return layers;
|
|
63
|
-
}
|
|
64
|
-
function initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster) {
|
|
65
|
-
if (typeof window === "undefined") return "ready";
|
|
66
|
-
if (!wantsMedia) return "ready";
|
|
67
|
-
if (resolved === "video") return mediaPoster ? "ready" : "pending";
|
|
68
|
-
if (typeof Image === "undefined") return "ready";
|
|
69
|
-
const cached = new Image();
|
|
70
|
-
cached.src = mediaUrl;
|
|
71
|
-
return cached.complete && cached.naturalWidth > 0 ? "ready" : "pending";
|
|
72
|
-
}
|
|
73
|
-
function getMediaBackgroundProps(variables) {
|
|
74
|
-
return {
|
|
75
|
-
mediaUrl: String(variables.mediaUrl || ""),
|
|
76
|
-
mediaType: String(variables.mediaType || "auto"),
|
|
77
|
-
mediaPoster: String(variables.mediaPoster || ""),
|
|
78
|
-
mediaPosition: String(variables.mediaPosition || "center"),
|
|
79
|
-
mediaTreatment: String(variables.mediaTreatment || "cinematic")
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
var SceneBackground = ({
|
|
83
|
-
style,
|
|
84
|
-
progress,
|
|
85
|
-
sceneDuration,
|
|
86
|
-
width: _width,
|
|
87
|
-
// accepted for symmetry; not currently used in render
|
|
88
|
-
height: _height,
|
|
89
|
-
mediaUrl = "",
|
|
90
|
-
mediaType = "auto",
|
|
91
|
-
mediaPoster,
|
|
92
|
-
mediaPosition = "center",
|
|
93
|
-
mediaTreatment = "cinematic",
|
|
94
|
-
textAnchor = "full",
|
|
95
|
-
backgroundEffect,
|
|
96
|
-
seed,
|
|
97
|
-
isPlaying = true,
|
|
98
|
-
beatIntensity = 0
|
|
99
|
-
}) => {
|
|
100
|
-
void _width;
|
|
101
|
-
void _height;
|
|
102
|
-
const resolved = resolveMediaType(mediaType, mediaUrl);
|
|
103
|
-
const wantsMedia = resolved !== "gradient" && !!mediaUrl;
|
|
104
|
-
const externalVideoBackdrop = useExternalVideoBackdrop();
|
|
105
|
-
const hasExternalVideoBackdrop = externalVideoBackdrop !== false && resolved === "video";
|
|
106
|
-
const externalVideoFailed = externalVideoBackdrop === "fallback" && resolved === "video";
|
|
107
|
-
const externalVideoReady = externalVideoBackdrop === "ready" && resolved === "video";
|
|
108
|
-
const [mediaPaint, setMediaPaint] = useState(
|
|
109
|
-
() => initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster)
|
|
110
|
-
);
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
setMediaPaint(initialMediaPaint(wantsMedia, resolved, mediaUrl, mediaPoster));
|
|
113
|
-
if (!wantsMedia || resolved !== "photo") return;
|
|
114
|
-
if (typeof Image === "undefined") return;
|
|
115
|
-
let cancelled = false;
|
|
116
|
-
const probe = new Image();
|
|
117
|
-
probe.onload = () => {
|
|
118
|
-
if (!cancelled) setMediaPaint("ready");
|
|
119
|
-
};
|
|
120
|
-
probe.onerror = () => {
|
|
121
|
-
if (!cancelled) setMediaPaint("failed");
|
|
122
|
-
};
|
|
123
|
-
probe.src = mediaUrl;
|
|
124
|
-
if (probe.complete) setMediaPaint(probe.naturalWidth > 0 ? "ready" : "failed");
|
|
125
|
-
return () => {
|
|
126
|
-
cancelled = true;
|
|
127
|
-
probe.onload = null;
|
|
128
|
-
probe.onerror = null;
|
|
129
|
-
};
|
|
130
|
-
}, [mediaUrl, mediaPoster, resolved, wantsMedia]);
|
|
131
|
-
const showMedia = wantsMedia && mediaPaint !== "failed";
|
|
132
|
-
const showTreatment = wantsMedia && mediaPaint === "ready";
|
|
133
|
-
const resolvedPosition = resolveMediaPosition(mediaPosition);
|
|
134
|
-
const resolvedTreatment = resolveMediaTreatment(mediaTreatment);
|
|
135
|
-
const treatmentLayers = getMediaTreatmentLayers(resolvedTreatment, textAnchor);
|
|
136
|
-
const gradSeed = typeof seed === "number" ? seed : typeof seed === "string" ? seed.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0) : 0;
|
|
137
|
-
const bgTransform = getBackgroundTransform(
|
|
138
|
-
backgroundEffect,
|
|
139
|
-
progress,
|
|
140
|
-
beatIntensity
|
|
141
|
-
);
|
|
142
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
143
|
-
(!hasExternalVideoBackdrop || externalVideoFailed) && /* @__PURE__ */ jsx(
|
|
144
|
-
BrandGradientOverlay,
|
|
145
|
-
{
|
|
146
|
-
style,
|
|
147
|
-
progress,
|
|
148
|
-
sceneDuration,
|
|
149
|
-
seed: gradSeed
|
|
150
|
-
}
|
|
151
|
-
),
|
|
152
|
-
showMedia && !hasExternalVideoBackdrop && (resolved === "video" ? /* @__PURE__ */ jsx(
|
|
153
|
-
SceneVideoBackdrop,
|
|
154
|
-
{
|
|
155
|
-
mediaUrl,
|
|
156
|
-
mediaPoster,
|
|
157
|
-
mediaPosition,
|
|
158
|
-
backgroundEffect,
|
|
159
|
-
progress,
|
|
160
|
-
sceneDuration,
|
|
161
|
-
beatIntensity,
|
|
162
|
-
isPlaying,
|
|
163
|
-
onReady: () => setMediaPaint("ready"),
|
|
164
|
-
onError: () => setMediaPaint("failed")
|
|
165
|
-
}
|
|
166
|
-
) : /* @__PURE__ */ jsx(
|
|
167
|
-
"img",
|
|
168
|
-
{
|
|
169
|
-
src: mediaUrl,
|
|
170
|
-
alt: "",
|
|
171
|
-
"aria-hidden": "true",
|
|
172
|
-
draggable: false,
|
|
173
|
-
"data-media-position": mediaPosition,
|
|
174
|
-
style: {
|
|
175
|
-
position: "absolute",
|
|
176
|
-
inset: 0,
|
|
177
|
-
transform: bgTransform.transform,
|
|
178
|
-
transformOrigin: bgTransform.transformOrigin,
|
|
179
|
-
width: "100%",
|
|
180
|
-
height: "100%",
|
|
181
|
-
objectFit: "cover",
|
|
182
|
-
objectPosition: resolvedPosition
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
)),
|
|
186
|
-
(hasExternalVideoBackdrop ? externalVideoReady : showTreatment) && !externalVideoFailed && treatmentLayers.map((layer) => /* @__PURE__ */ jsx(
|
|
187
|
-
"div",
|
|
188
|
-
{
|
|
189
|
-
"data-media-treatment": resolvedTreatment,
|
|
190
|
-
"data-media-overlay": layer.id,
|
|
191
|
-
style: {
|
|
192
|
-
position: "absolute",
|
|
193
|
-
inset: 0,
|
|
194
|
-
background: layer.background,
|
|
195
|
-
pointerEvents: "none",
|
|
196
|
-
...layer.style
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
layer.id
|
|
200
|
-
))
|
|
201
|
-
] });
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
export {
|
|
205
|
-
getMediaBackgroundProps,
|
|
206
|
-
SceneBackground
|
|
207
|
-
};
|
|
@@ -1,37 +1,3 @@
|
|
|
1
|
-
// src/visual-system/scene-templates/external-video-backdrop.tsx
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { jsx } from "react/jsx-runtime";
|
|
4
|
-
var DEFAULT = { mode: false, audioMuted: true, audioVolume: 1 };
|
|
5
|
-
var sharedContext = globalThis;
|
|
6
|
-
var BackdropContext = sharedContext.__vanillaskyVideoBackdropContext ??= React.createContext(DEFAULT);
|
|
7
|
-
function ExternalVideoBackdropProvider({
|
|
8
|
-
mode,
|
|
9
|
-
audioMuted = true,
|
|
10
|
-
audioVolume = 1,
|
|
11
|
-
preparingNarration = false,
|
|
12
|
-
onMediaError,
|
|
13
|
-
children
|
|
14
|
-
}) {
|
|
15
|
-
const value = React.useMemo(
|
|
16
|
-
() => ({ mode, audioMuted, audioVolume, preparingNarration, onMediaError }),
|
|
17
|
-
[mode, audioMuted, audioVolume, preparingNarration, onMediaError]
|
|
18
|
-
);
|
|
19
|
-
return /* @__PURE__ */ jsx(BackdropContext.Provider, { value, children });
|
|
20
|
-
}
|
|
21
|
-
function useExternalVideoBackdrop() {
|
|
22
|
-
return React.useContext(BackdropContext).mode;
|
|
23
|
-
}
|
|
24
|
-
function useMediaAudio() {
|
|
25
|
-
const { audioMuted, audioVolume } = React.useContext(BackdropContext);
|
|
26
|
-
return { muted: audioMuted, volume: audioVolume };
|
|
27
|
-
}
|
|
28
|
-
function useNarrationPreroll() {
|
|
29
|
-
return React.useContext(BackdropContext).preparingNarration === true;
|
|
30
|
-
}
|
|
31
|
-
function useMediaFailure() {
|
|
32
|
-
return React.useContext(BackdropContext).onMediaError;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
1
|
// src/visual-system/scene-templates/tokens.ts
|
|
36
2
|
var STYLE_PRESETS = {
|
|
37
3
|
bold: {
|
|
@@ -144,6 +110,40 @@ function darken(hex, factor) {
|
|
|
144
110
|
return `#${ch(1)}${ch(3)}${ch(5)}`;
|
|
145
111
|
}
|
|
146
112
|
|
|
113
|
+
// src/visual-system/scene-templates/external-video-backdrop.tsx
|
|
114
|
+
import React from "react";
|
|
115
|
+
import { jsx } from "react/jsx-runtime";
|
|
116
|
+
var DEFAULT = { mode: false, audioMuted: true, audioVolume: 1 };
|
|
117
|
+
var sharedContext = globalThis;
|
|
118
|
+
var BackdropContext = sharedContext.__vanillaskyVideoBackdropContext ??= React.createContext(DEFAULT);
|
|
119
|
+
function ExternalVideoBackdropProvider({
|
|
120
|
+
mode,
|
|
121
|
+
audioMuted = true,
|
|
122
|
+
audioVolume = 1,
|
|
123
|
+
preparingNarration = false,
|
|
124
|
+
onMediaError,
|
|
125
|
+
children
|
|
126
|
+
}) {
|
|
127
|
+
const value = React.useMemo(
|
|
128
|
+
() => ({ mode, audioMuted, audioVolume, preparingNarration, onMediaError }),
|
|
129
|
+
[mode, audioMuted, audioVolume, preparingNarration, onMediaError]
|
|
130
|
+
);
|
|
131
|
+
return /* @__PURE__ */ jsx(BackdropContext.Provider, { value, children });
|
|
132
|
+
}
|
|
133
|
+
function useExternalVideoBackdrop() {
|
|
134
|
+
return React.useContext(BackdropContext).mode;
|
|
135
|
+
}
|
|
136
|
+
function useMediaAudio() {
|
|
137
|
+
const { audioMuted, audioVolume } = React.useContext(BackdropContext);
|
|
138
|
+
return { muted: audioMuted, volume: audioVolume };
|
|
139
|
+
}
|
|
140
|
+
function useNarrationPreroll() {
|
|
141
|
+
return React.useContext(BackdropContext).preparingNarration === true;
|
|
142
|
+
}
|
|
143
|
+
function useMediaFailure() {
|
|
144
|
+
return React.useContext(BackdropContext).onMediaError;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
147
|
export {
|
|
148
148
|
resolveDensity,
|
|
149
149
|
resolveTokens,
|