diff --git a/public/wallpapers/wispysky.mp4 b/public/wallpapers/wispysky.mp4
new file mode 100644
index 00000000..3454d916
Binary files /dev/null and b/public/wallpapers/wispysky.mp4 differ
diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx
index b69c420f..c8c22f78 100644
--- a/src/components/video-editor/SettingsPanel.tsx
+++ b/src/components/video-editor/SettingsPanel.tsx
@@ -1,4 +1,4 @@
-import { Film, Palette, Trash2, Upload, X } from "lucide-react";
+import { 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";
@@ -802,12 +802,16 @@ export function SettingsPanel({
>
- Video backgrounds coming next.
+
+
+
+
+ {customImages.filter(isVideoWallpaperSource).map((videoUrl, idx) => {
+ const isSelected = getWallpaperTileState(videoUrl);
+ return renderWallpaperImageTile(videoUrl, isSelected, {
+ key: `custom-video-${idx}`,
+ ariaLabel: videoUrl.split(/[\\/]/).pop() ?? "Video background",
+ title: videoUrl.split(/[\\/]/).pop(),
+ onClick: () => onWallpaperChange(videoUrl),
+ children: (
+
+ ),
+ });
+ })}
+
+ {BUILT_IN_WALLPAPERS.filter(w => isVideoWallpaperSource(w.publicPath)).map((wallpaper) => {
+ const wallpaperValue = wallpaper.publicPath;
+ const isSelected = selected === wallpaperValue;
+ return renderWallpaperImageTile(wallpaperValue, isSelected, {
+ key: wallpaperValue,
+ ariaLabel: wallpaper.label,
+ title: wallpaper.label,
+ onClick: () => onWallpaperChange(wallpaperValue),
+ });
+ })}
+
) : backgroundTab === "color" ? (
diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx
index e4c6b647..9c9691fa 100644
--- a/src/components/video-editor/VideoPlayback.tsx
+++ b/src/components/video-editor/VideoPlayback.tsx
@@ -15,7 +15,6 @@ import {
DEFAULT_WALLPAPER_RELATIVE_PATH,
isVideoWallpaperSource,
} from "@/lib/wallpapers";
-import { resolveMediaElementSource } from "@/lib/exporter/localMediaSource";
import {
Application,
Container,
@@ -329,6 +328,7 @@ const VideoPlayback = forwardRef(
const trimRegionsRef = useRef([]);
const speedRegionsRef = useRef([]);
const lastWebcamSyncTimeRef = useRef(null);
+ const bgVideoRef = useRef(null);
const zoomMotionBlurRef = useRef(zoomMotionBlur);
const connectZoomsRef = useRef(connectZooms);
const zoomInDurationMsRef = useRef(zoomInDurationMs);
@@ -753,8 +753,25 @@ const VideoPlayback = forwardRef(
useEffect(() => {
isPlayingRef.current = isPlaying;
+ const bgVideo = bgVideoRef.current;
+ if (bgVideo) {
+ if (isPlaying) {
+ bgVideo.play().catch(() => {});
+ } else {
+ bgVideo.pause();
+ }
+ }
}, [isPlaying]);
+ // Sync background video wallpaper with timeline scrubbing
+ useEffect(() => {
+ const bgVideo = bgVideoRef.current;
+ if (!bgVideo) return;
+ if (!isPlaying && bgVideo.duration && Number.isFinite(bgVideo.duration)) {
+ bgVideo.currentTime = currentTime % bgVideo.duration;
+ }
+ }, [currentTime, isPlaying]);
+
useEffect(() => {
trimRegionsRef.current = trimRegions;
}, [trimRegions]);
@@ -1506,10 +1523,12 @@ const VideoPlayback = forwardRef(
}
if (isVideoWallpaperSource(wallpaper)) {
- const resolved = await resolveMediaElementSource(wallpaper);
- revokeResolvedWallpaper = resolved.revoke;
+ let videoSrc = wallpaper;
+ if (wallpaper.startsWith("/") && !wallpaper.startsWith("//")) {
+ videoSrc = await getAssetPath(wallpaper.replace(/^\//, ""));
+ }
if (mounted) {
- setResolvedWallpaper(resolved.src);
+ setResolvedWallpaper(videoSrc);
setResolvedWallpaperKind("video");
}
return;
@@ -1610,11 +1629,11 @@ const VideoPlayback = forwardRef(
{resolvedWallpaperKind === "video" && resolvedWallpaper ? (