diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx
index 39d6d080..b69c420f 100644
--- a/src/components/video-editor/SettingsPanel.tsx
+++ b/src/components/video-editor/SettingsPanel.tsx
@@ -1,4 +1,4 @@
-import { Palette, Trash2, Upload, X } from "lucide-react";
+import { Film, Palette, Trash2, Upload, X } from "lucide-react";
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
@@ -15,7 +15,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { getAssetPath, getRenderableAssetUrl } from "@/lib/assetPath";
import { cn } from "@/lib/utils";
import type { BuiltInWallpaper } from "@/lib/wallpapers";
-import { BUILT_IN_WALLPAPERS, getAvailableWallpapers } from "@/lib/wallpapers";
+import { BUILT_IN_WALLPAPERS, getAvailableWallpapers, isVideoWallpaperSource } from "@/lib/wallpapers";
import { type AspectRatio } from "@/utils/aspectRatioUtils";
import minimalCursorUrl from "../../../Minimal Cursor.svg";
import amongusCursorUrl from "../../assets/cursors/amongus/default.png";
@@ -105,7 +105,7 @@ const CAPTION_ANIMATION_OPTIONS: Array<{ value: AutoCaptionAnimation; label: str
{ value: "pop", label: "Pop" },
];
-type BackgroundTab = "image" | "color" | "gradient";
+type BackgroundTab = "image" | "video" | "color" | "gradient";
export type EditorEffectSection =
| "scene"
| "cursor"
@@ -128,6 +128,10 @@ function getBackgroundTabForWallpaper(value: string): BackgroundTab {
return "color";
}
+ if (isVideoWallpaperSource(value)) {
+ return "video";
+ }
+
return "image";
}
@@ -713,7 +717,18 @@ export function SettingsPanel({
if (selected.startsWith("data:image") && !customImages.includes(selected)) {
setCustomImages((prev) => [selected, ...prev]);
}
- }, [customImages, selected]);
+
+ const isBuiltInWallpaper =
+ builtInWallpaperPaths.includes(selected) || wallpaperPreviewPaths.includes(selected);
+
+ if (
+ !isBuiltInWallpaper &&
+ isVideoWallpaperSource(selected) &&
+ !customImages.includes(selected)
+ ) {
+ setCustomImages((prev) => [selected, ...prev]);
+ }
+ }, [builtInWallpaperPaths, customImages, selected, wallpaperPreviewPaths]);
useEffect(() => {
saveEditorPreferences({ customWallpapers: customImages });
@@ -767,7 +782,7 @@ export function SettingsPanel({
);
const renderWallpaperImageTile = (
- imageUrl: string,
+ wallpaperUrl: string,
isSelected: boolean,
props?: {
key?: string;
@@ -786,16 +801,25 @@ export function SettingsPanel({
role="button"
>
-

+ {isVideoWallpaperSource(wallpaperUrl) ? (
+
+
+
+ {props?.title ?? props?.ariaLabel ?? tSettings("background.video", "Video background")}
+
+
+ ) : (
+

+ )}
{props?.children}
@@ -1000,10 +1024,11 @@ export function SettingsPanel({
-
+
{(
[
{ value: "image", label: tSettings("background.image") },
+ { value: "video", label: tSettings("background.video", "Video") },
{ value: "color", label: tSettings("background.color") },
{ value: "gradient", label: tSettings("background.gradient") },
] as const
@@ -1069,6 +1094,12 @@ export function SettingsPanel({
const isSelected = getWallpaperTileState(imageUrl);
return renderWallpaperImageTile(imageUrl, isSelected, {
key: `custom-${idx}`,
+ ariaLabel: isVideoWallpaperSource(imageUrl)
+ ? imageUrl.split(/[\\/]/).pop() ?? tSettings("background.video", "Video background")
+ : undefined,
+ title: isVideoWallpaperSource(imageUrl)
+ ? imageUrl.split(/[\\/]/).pop()
+ : undefined,
onClick: () => onWallpaperChange(imageUrl),
children: (
+ ) : backgroundTab === "video" ? (
+
+ Video backgrounds coming next.
+
) : backgroundTab === "color" ? (
(
const [resolvedWallpaper, setResolvedWallpaper] = useState
(
null,
);
+ const [resolvedWallpaperKind, setResolvedWallpaperKind] = useState<
+ "image" | "video" | "style"
+ >("image");
useEffect(() => {
let mounted = true;
+ let revokeResolvedWallpaper = () => {};
(async () => {
try {
if (!wallpaper) {
const def = await getAssetPath(DEFAULT_WALLPAPER_RELATIVE_PATH);
- if (mounted) setResolvedWallpaper(def);
+ if (mounted) {
+ setResolvedWallpaper(def);
+ setResolvedWallpaperKind("image");
+ }
return;
}
@@ -1489,13 +1498,29 @@ const VideoPlayback = forwardRef(
wallpaper.startsWith("linear-gradient") ||
wallpaper.startsWith("radial-gradient")
) {
- if (mounted) setResolvedWallpaper(wallpaper);
+ if (mounted) {
+ setResolvedWallpaper(wallpaper);
+ setResolvedWallpaperKind("style");
+ }
+ return;
+ }
+
+ if (isVideoWallpaperSource(wallpaper)) {
+ const resolved = await resolveMediaElementSource(wallpaper);
+ revokeResolvedWallpaper = resolved.revoke;
+ if (mounted) {
+ setResolvedWallpaper(resolved.src);
+ setResolvedWallpaperKind("video");
+ }
return;
}
// If it's a data URL (custom uploaded image), use as-is
if (wallpaper.startsWith("data:")) {
- if (mounted) setResolvedWallpaper(wallpaper);
+ if (mounted) {
+ setResolvedWallpaper(wallpaper);
+ setResolvedWallpaperKind("image");
+ }
return;
}
@@ -1505,20 +1530,31 @@ const VideoPlayback = forwardRef(
wallpaper.startsWith("/")
) {
const renderable = await getRenderableAssetUrl(wallpaper);
- if (mounted) setResolvedWallpaper(renderable);
+ if (mounted) {
+ setResolvedWallpaper(renderable);
+ setResolvedWallpaperKind("image");
+ }
return;
}
const p = await getRenderableAssetUrl(
await getAssetPath(wallpaper.replace(/^\//, "")),
);
- if (mounted) setResolvedWallpaper(p);
+ if (mounted) {
+ setResolvedWallpaper(p);
+ setResolvedWallpaperKind("image");
+ }
} catch (err) {
- if (mounted)
+ if (mounted) {
setResolvedWallpaper(wallpaper || DEFAULT_WALLPAPER_PATH);
+ setResolvedWallpaperKind(
+ isVideoWallpaperSource(wallpaper || "") ? "video" : "image",
+ );
+ }
}
})();
return () => {
mounted = false;
+ revokeResolvedWallpaper();
};
}, [wallpaper]);
@@ -1531,7 +1567,7 @@ const VideoPlayback = forwardRef(
};
}, []);
- const isImageUrl = Boolean(
+ const isImageUrl = resolvedWallpaperKind === "image" && Boolean(
resolvedWallpaper &&
(resolvedWallpaper.startsWith("file://") ||
resolvedWallpaper.startsWith("http") ||
@@ -1540,7 +1576,9 @@ const VideoPlayback = forwardRef(
);
const backgroundStyle = isImageUrl
? { backgroundImage: `url(${resolvedWallpaper || ""})` }
- : { background: resolvedWallpaper || "" };
+ : resolvedWallpaperKind === "video"
+ ? {}
+ : { background: resolvedWallpaper || "" };
const nativeAspectRatio = (() => {
const locked = lockedVideoDimensionsRef.current;
@@ -1569,13 +1607,28 @@ const VideoPlayback = forwardRef(
}}
>
{/* Background layer */}
- 0 ? `blur(${backgroundBlur}px)` : "none",
- }}
- />
+ {resolvedWallpaperKind === "video" && resolvedWallpaper ? (
+