mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 07:45:34 +00:00
Fix black video wallpaper exports
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getRenderableVideoUrl } from "./assetPath";
|
||||
import { getExportableVideoUrl, getRenderableVideoUrl } from "./assetPath";
|
||||
|
||||
describe("getRenderableVideoUrl", () => {
|
||||
beforeEach(() => {
|
||||
@@ -39,3 +39,27 @@ describe("getRenderableVideoUrl", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getExportableVideoUrl", () => {
|
||||
beforeEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("uses a direct file URL for absolute local video paths", async () => {
|
||||
await expect(getExportableVideoUrl("/Users/egg/Desktop/bg.mp4")).resolves.toBe(
|
||||
"file:///Users/egg/Desktop/bg.mp4",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps bundled wallpaper paths routed through the app asset directory", async () => {
|
||||
vi.stubGlobal("window", {
|
||||
location: {
|
||||
protocol: "http:",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(getExportableVideoUrl("/wallpapers/wispysky.mp4")).resolves.toBe(
|
||||
"/wallpapers/wispysky.mp4",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+27
-4
@@ -177,6 +177,10 @@ async function resolveLocalMediaUrl(filePath: string): Promise<string> {
|
||||
return toFileUrl(filePath);
|
||||
}
|
||||
|
||||
function isBundledAssetPath(asset: string): boolean {
|
||||
return asset.startsWith("/wallpapers/") || asset.startsWith("/app-icons/");
|
||||
}
|
||||
|
||||
export async function getRenderableVideoUrl(asset: string): Promise<string> {
|
||||
if (
|
||||
!asset ||
|
||||
@@ -188,10 +192,7 @@ export async function getRenderableVideoUrl(asset: string): Promise<string> {
|
||||
return asset;
|
||||
}
|
||||
|
||||
const isBundledAssetPath =
|
||||
asset.startsWith("/wallpapers/") || asset.startsWith("/app-icons/");
|
||||
|
||||
if (isAbsoluteLocalAssetPath(asset) && !isBundledAssetPath) {
|
||||
if (isAbsoluteLocalAssetPath(asset) && !isBundledAssetPath(asset)) {
|
||||
return resolveLocalMediaUrl(asset);
|
||||
}
|
||||
|
||||
@@ -202,6 +203,28 @@ export async function getRenderableVideoUrl(asset: string): Promise<string> {
|
||||
return asset;
|
||||
}
|
||||
|
||||
export async function getExportableVideoUrl(asset: string): Promise<string> {
|
||||
if (
|
||||
!asset ||
|
||||
asset.startsWith("blob:") ||
|
||||
asset.startsWith("data:") ||
|
||||
asset.startsWith("file://") ||
|
||||
asset.startsWith("http")
|
||||
) {
|
||||
return asset;
|
||||
}
|
||||
|
||||
if (isAbsoluteLocalAssetPath(asset) && !isBundledAssetPath(asset)) {
|
||||
return toFileUrl(asset);
|
||||
}
|
||||
|
||||
if (asset.startsWith("/") && !asset.startsWith("//")) {
|
||||
return getAssetPath(asset.replace(/^\/+/, ""));
|
||||
}
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wallpaper thumbnail helper — generates a tiny JPEG thumbnail via the main
|
||||
// process (nativeImage resize) and returns a data URL for fast grid rendering.
|
||||
|
||||
@@ -18,6 +18,7 @@ vi.mock("pixi-filters/motion-blur", () => ({
|
||||
|
||||
vi.mock("@/lib/assetPath", () => ({
|
||||
getAssetPath: vi.fn(async (value: string) => value),
|
||||
getExportableVideoUrl: vi.fn(async (value: string) => value),
|
||||
getRenderableAssetUrl: vi.fn((value: string) => value),
|
||||
}));
|
||||
|
||||
|
||||
@@ -51,7 +51,11 @@ import {
|
||||
getWebcamOverlaySizePx,
|
||||
} from "@/components/video-editor/webcamOverlay";
|
||||
import { getWebcamMediaTargetTimeSeconds } from "@/components/video-editor/videoPlayback/webcamSync";
|
||||
import { getAssetPath, getRenderableAssetUrl, getRenderableVideoUrl } from "@/lib/assetPath";
|
||||
import {
|
||||
getAssetPath,
|
||||
getExportableVideoUrl,
|
||||
getRenderableAssetUrl,
|
||||
} from "@/lib/assetPath";
|
||||
import { extensionHost } from "@/lib/extensions";
|
||||
import {
|
||||
mapCursorToCanvasNormalized,
|
||||
@@ -374,17 +378,39 @@ export class FrameRenderer {
|
||||
videoSrc = await getAssetPath(wallpaper.replace(/^\//, ""));
|
||||
}
|
||||
|
||||
const backgroundSource = await resolveMediaElementSource(videoSrc);
|
||||
this.cleanupBackgroundSource = backgroundSource.revoke;
|
||||
|
||||
const video = document.createElement("video");
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.playsInline = true;
|
||||
video.preload = "auto";
|
||||
video.src = videoSrc;
|
||||
video.src = backgroundSource.src;
|
||||
video.load();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
video.onloadeddata = () => resolve();
|
||||
video.onerror = () =>
|
||||
const onReady = () => {
|
||||
if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const onError = () => {
|
||||
cleanup();
|
||||
reject(new Error(`Failed to load video wallpaper: ${wallpaper}`));
|
||||
};
|
||||
const cleanup = () => {
|
||||
video.removeEventListener("loadeddata", onReady);
|
||||
video.removeEventListener("canplay", onReady);
|
||||
video.removeEventListener("error", onError);
|
||||
};
|
||||
|
||||
video.addEventListener("loadeddata", onReady);
|
||||
video.addEventListener("canplay", onReady);
|
||||
video.addEventListener("error", onError);
|
||||
onReady();
|
||||
});
|
||||
|
||||
this.backgroundVideoElement = video;
|
||||
@@ -581,7 +607,7 @@ export class FrameRenderer {
|
||||
}
|
||||
|
||||
if (isVideoWallpaperSource(wallpaper)) {
|
||||
return getRenderableVideoUrl(wallpaper);
|
||||
return getExportableVideoUrl(wallpaper);
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -36,6 +36,7 @@ vi.mock("pixi-filters/motion-blur", () => ({
|
||||
|
||||
vi.mock("@/lib/assetPath", () => ({
|
||||
getAssetPath: vi.fn(async (value: string) => value),
|
||||
getExportableVideoUrl: vi.fn(async (value: string) => value),
|
||||
getRenderableAssetUrl: vi.fn((value: string) => value),
|
||||
}));
|
||||
|
||||
|
||||
@@ -57,7 +57,11 @@ import {
|
||||
getWebcamOverlaySizePx,
|
||||
} from "@/components/video-editor/webcamOverlay";
|
||||
import { getWebcamMediaTargetTimeSeconds } from "@/components/video-editor/videoPlayback/webcamSync";
|
||||
import { getAssetPath, getRenderableAssetUrl, getRenderableVideoUrl } from "@/lib/assetPath";
|
||||
import {
|
||||
getAssetPath,
|
||||
getExportableVideoUrl,
|
||||
getRenderableAssetUrl,
|
||||
} from "@/lib/assetPath";
|
||||
import { extensionHost } from "@/lib/extensions";
|
||||
import {
|
||||
mapCursorToCanvasNormalized,
|
||||
@@ -332,6 +336,7 @@ export class FrameRenderer {
|
||||
private backgroundForwardFrameSource: ForwardFrameSource | null = null;
|
||||
private backgroundDecodedFrame: VideoFrame | null = null;
|
||||
private backgroundVideoElement: HTMLVideoElement | null = null;
|
||||
private cleanupBackgroundSource: (() => void) | null = null;
|
||||
private videoShadowLayers: ShadowLayer[] = [];
|
||||
private webcamShadowLayers: ShadowLayer[] = [];
|
||||
private webcamSprite: Sprite | null = null;
|
||||
@@ -344,6 +349,8 @@ export class FrameRenderer {
|
||||
private webcamFrameCacheCtx: CanvasRenderingContext2D | null = null;
|
||||
private sceneVideoFrameStagingCanvas: HTMLCanvasElement | null = null;
|
||||
private sceneVideoFrameStagingCtx: CanvasRenderingContext2D | null = null;
|
||||
private backgroundVideoFrameStagingCanvas: HTMLCanvasElement | null = null;
|
||||
private backgroundVideoFrameStagingCtx: CanvasRenderingContext2D | null = null;
|
||||
private webcamVideoFrameStagingCanvas: HTMLCanvasElement | null = null;
|
||||
private webcamVideoFrameStagingCtx: CanvasRenderingContext2D | null = null;
|
||||
private captionMeasureCanvas: HTMLCanvasElement | null = null;
|
||||
@@ -708,7 +715,7 @@ export class FrameRenderer {
|
||||
}
|
||||
|
||||
private ensureVideoFrameStagingCanvas(
|
||||
kind: "scene" | "webcam",
|
||||
kind: "scene" | "background" | "webcam",
|
||||
width: number,
|
||||
height: number,
|
||||
): { canvas: HTMLCanvasElement; context: CanvasRenderingContext2D } | null {
|
||||
@@ -717,9 +724,15 @@ export class FrameRenderer {
|
||||
const currentCanvas =
|
||||
kind === "scene"
|
||||
? this.sceneVideoFrameStagingCanvas
|
||||
: this.webcamVideoFrameStagingCanvas;
|
||||
: kind === "background"
|
||||
? this.backgroundVideoFrameStagingCanvas
|
||||
: this.webcamVideoFrameStagingCanvas;
|
||||
const currentContext =
|
||||
kind === "scene" ? this.sceneVideoFrameStagingCtx : this.webcamVideoFrameStagingCtx;
|
||||
kind === "scene"
|
||||
? this.sceneVideoFrameStagingCtx
|
||||
: kind === "background"
|
||||
? this.backgroundVideoFrameStagingCtx
|
||||
: this.webcamVideoFrameStagingCtx;
|
||||
|
||||
if (
|
||||
currentCanvas &&
|
||||
@@ -741,6 +754,9 @@ export class FrameRenderer {
|
||||
if (kind === "scene") {
|
||||
this.sceneVideoFrameStagingCanvas = canvas;
|
||||
this.sceneVideoFrameStagingCtx = context;
|
||||
} else if (kind === "background") {
|
||||
this.backgroundVideoFrameStagingCanvas = canvas;
|
||||
this.backgroundVideoFrameStagingCtx = context;
|
||||
} else {
|
||||
this.webcamVideoFrameStagingCanvas = canvas;
|
||||
this.webcamVideoFrameStagingCtx = context;
|
||||
@@ -751,7 +767,7 @@ export class FrameRenderer {
|
||||
|
||||
private stageVideoFrameForTexture(
|
||||
frame: VideoFrame,
|
||||
kind: "scene" | "webcam",
|
||||
kind: "scene" | "background" | "webcam",
|
||||
fallbackWidth: number,
|
||||
fallbackHeight: number,
|
||||
): CanvasImageSource | VideoFrame {
|
||||
@@ -814,6 +830,8 @@ export class FrameRenderer {
|
||||
void this.backgroundForwardFrameSource?.destroy();
|
||||
this.backgroundForwardFrameSource = null;
|
||||
this.closeBackgroundDecodedFrame();
|
||||
this.cleanupBackgroundSource?.();
|
||||
this.cleanupBackgroundSource = null;
|
||||
|
||||
if (this.backgroundVideoElement) {
|
||||
this.backgroundVideoElement.pause();
|
||||
@@ -856,17 +874,39 @@ export class FrameRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
const backgroundSource = await resolveMediaElementSource(videoSrc);
|
||||
this.cleanupBackgroundSource = backgroundSource.revoke;
|
||||
|
||||
const video = document.createElement("video");
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.playsInline = true;
|
||||
video.preload = "auto";
|
||||
video.src = videoSrc;
|
||||
video.src = backgroundSource.src;
|
||||
video.load();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
video.onloadeddata = () => resolve();
|
||||
video.onerror = () =>
|
||||
const onReady = () => {
|
||||
if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const onError = () => {
|
||||
cleanup();
|
||||
reject(new Error(`Failed to load video wallpaper: ${wallpaper}`));
|
||||
};
|
||||
const cleanup = () => {
|
||||
video.removeEventListener("loadeddata", onReady);
|
||||
video.removeEventListener("canplay", onReady);
|
||||
video.removeEventListener("error", onError);
|
||||
};
|
||||
|
||||
video.addEventListener("loadeddata", onReady);
|
||||
video.addEventListener("canplay", onReady);
|
||||
video.addEventListener("error", onError);
|
||||
onReady();
|
||||
});
|
||||
|
||||
this.backgroundVideoElement = video;
|
||||
@@ -1434,8 +1474,14 @@ export class FrameRenderer {
|
||||
this.closeBackgroundDecodedFrame();
|
||||
this.backgroundDecodedFrame = decodedFrame;
|
||||
if (decodedFrame) {
|
||||
this.ensureBackgroundSprite(
|
||||
const resolvedBackgroundSource = this.stageVideoFrameForTexture(
|
||||
decodedFrame,
|
||||
"background",
|
||||
decodedFrame.displayWidth,
|
||||
decodedFrame.displayHeight,
|
||||
);
|
||||
this.ensureBackgroundSprite(
|
||||
resolvedBackgroundSource,
|
||||
decodedFrame.displayWidth,
|
||||
decodedFrame.displayHeight,
|
||||
);
|
||||
@@ -1488,7 +1534,7 @@ export class FrameRenderer {
|
||||
}
|
||||
|
||||
if (isVideoWallpaperSource(wallpaper)) {
|
||||
return getRenderableVideoUrl(wallpaper);
|
||||
return getExportableVideoUrl(wallpaper);
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user