mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 23:35:43 +00:00
fix(export): parse native gradient backgrounds
This commit is contained in:
@@ -54,6 +54,10 @@ function createExporter(overrides: Record<string, unknown> = {}) {
|
||||
effectiveDurationSec: number,
|
||||
) => string | null;
|
||||
resolveNativeStaticLayoutBackground: () => Promise<unknown>;
|
||||
createNativeStaticLayoutGradient: (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
wallpaper: string,
|
||||
) => CanvasGradient | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -243,6 +247,26 @@ describe("ModernVideoExporter native static-layout eligibility", () => {
|
||||
expect(electronAPI.closeExportStream).toHaveBeenCalledWith("background-stream");
|
||||
});
|
||||
|
||||
it("parses rgba color stops in native gradient backgrounds", () => {
|
||||
const exporter = createExporter();
|
||||
const gradient = { addColorStop: vi.fn() };
|
||||
const ctx = {
|
||||
createLinearGradient: vi.fn(() => gradient),
|
||||
createRadialGradient: vi.fn(() => gradient),
|
||||
} as unknown as CanvasRenderingContext2D;
|
||||
|
||||
const result = exporter.createNativeStaticLayoutGradient(
|
||||
ctx,
|
||||
"linear-gradient( 111.6deg, rgba(114,167,232,1) 9.4%, rgba(253,129,82,1) 43.9%, rgba(249,202,86,1) 86.3% )",
|
||||
);
|
||||
|
||||
expect(result).toBe(gradient);
|
||||
expect(gradient.addColorStop).toHaveBeenCalledTimes(3);
|
||||
expect(gradient.addColorStop).toHaveBeenNthCalledWith(1, 0, "rgba(114,167,232,1)");
|
||||
expect(gradient.addColorStop).toHaveBeenNthCalledWith(2, 0.5, "rgba(253,129,82,1)");
|
||||
expect(gradient.addColorStop).toHaveBeenNthCalledWith(3, 1, "rgba(249,202,86,1)");
|
||||
});
|
||||
|
||||
it("allows non-tail trim timelines with native static-layout", () => {
|
||||
const exporter = createExporter({
|
||||
trimRegions: [{ id: "trim-1", startMs: 10_000, endMs: 12_000 }],
|
||||
|
||||
@@ -281,6 +281,7 @@ export class ModernVideoExporter {
|
||||
private maxNativeWriteInFlight = 1;
|
||||
private lastNativeExportError: string | null = null;
|
||||
private nativeStaticLayoutSkipReason: string | null = null;
|
||||
private nativeStaticLayoutBackgroundSkipReason: string | null = null;
|
||||
private nativeH264Encoder: VideoEncoder | null = null;
|
||||
private nativeEncoderError: Error | null = null;
|
||||
private effectiveDurationSec = 0;
|
||||
@@ -318,6 +319,7 @@ export class ModernVideoExporter {
|
||||
this.encoderError = null;
|
||||
this.nativeEncoderError = null;
|
||||
this.nativeStaticLayoutSkipReason = null;
|
||||
this.nativeStaticLayoutBackgroundSkipReason = null;
|
||||
this.totalExportStartTimeMs = this.getNowMs();
|
||||
const backendPreference = this.config.backendPreference ?? "auto";
|
||||
let useNativeEncoder = false;
|
||||
@@ -1267,6 +1269,7 @@ export class ModernVideoExporter {
|
||||
}
|
||||
|
||||
private async resolveNativeStaticLayoutBackground(): Promise<NativeStaticLayoutBackground | null> {
|
||||
this.nativeStaticLayoutBackgroundSkipReason = null;
|
||||
const configuredWallpaper = this.config.wallpaper?.trim() ?? "";
|
||||
const wallpaper = configuredWallpaper || DEFAULT_WALLPAPER_PATH;
|
||||
if (/^#?[0-9a-f]{6}$/i.test(wallpaper)) {
|
||||
@@ -1277,21 +1280,38 @@ export class ModernVideoExporter {
|
||||
}
|
||||
|
||||
if (wallpaper.startsWith("data:image/") || wallpaper.startsWith("blob:")) {
|
||||
return this.materializeNativeStaticLayoutImageSource(wallpaper);
|
||||
const materialized = await this.materializeNativeStaticLayoutImageSource(wallpaper);
|
||||
if (materialized) {
|
||||
return materialized;
|
||||
}
|
||||
this.nativeStaticLayoutBackgroundSkipReason =
|
||||
"unsupported-background-image-materialize-failed";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wallpaper.startsWith("linear-gradient") || wallpaper.startsWith("radial-gradient")) {
|
||||
return this.materializeNativeStaticLayoutGradientBackground(wallpaper);
|
||||
const materialized =
|
||||
await this.materializeNativeStaticLayoutGradientBackground(wallpaper);
|
||||
if (materialized) {
|
||||
return materialized;
|
||||
}
|
||||
this.nativeStaticLayoutBackgroundSkipReason =
|
||||
"unsupported-background-gradient-materialize-failed";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
isVideoWallpaperSource(wallpaper) ||
|
||||
wallpaper.startsWith("data:") ||
|
||||
wallpaper.startsWith("blob:") ||
|
||||
wallpaper.startsWith("http") ||
|
||||
wallpaper.startsWith("linear-gradient") ||
|
||||
wallpaper.startsWith("radial-gradient")
|
||||
) {
|
||||
if (isVideoWallpaperSource(wallpaper)) {
|
||||
this.nativeStaticLayoutBackgroundSkipReason = "unsupported-background-video";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wallpaper.startsWith("data:") || wallpaper.startsWith("blob:")) {
|
||||
this.nativeStaticLayoutBackgroundSkipReason = "unsupported-background-data-or-blob";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wallpaper.startsWith("http")) {
|
||||
this.nativeStaticLayoutBackgroundSkipReason = "unsupported-background-remote";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1313,7 +1333,12 @@ export class ModernVideoExporter {
|
||||
}
|
||||
|
||||
const localPath = getLocalFilePath(wallpaper);
|
||||
return localPath ? { backgroundColor: "#101010", backgroundImagePath: localPath } : null;
|
||||
if (localPath) {
|
||||
return { backgroundColor: "#101010", backgroundImagePath: localPath };
|
||||
}
|
||||
|
||||
this.nativeStaticLayoutBackgroundSkipReason = "unsupported-background-local-path";
|
||||
return null;
|
||||
}
|
||||
|
||||
private async materializeNativeStaticLayoutImageSource(
|
||||
@@ -1414,9 +1439,12 @@ export class ModernVideoExporter {
|
||||
}
|
||||
|
||||
const [, type, params] = gradientMatch;
|
||||
const parts = params.split(",").map((part) => part.trim());
|
||||
const parts = this.splitCssGradientArguments(params).map((part) => part.trim());
|
||||
const colorStops = parts
|
||||
.map((part) => part.match(/^(#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|[a-z]+)/)?.[1])
|
||||
.map(
|
||||
(part) =>
|
||||
part.match(/^(#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|[a-z]+)/i)?.[1],
|
||||
)
|
||||
.filter((color): color is string => Boolean(color));
|
||||
if (colorStops.length === 0) {
|
||||
return null;
|
||||
@@ -1446,6 +1474,40 @@ export class ModernVideoExporter {
|
||||
return gradient;
|
||||
}
|
||||
|
||||
private splitCssGradientArguments(params: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let current = "";
|
||||
let depth = 0;
|
||||
|
||||
for (const char of params) {
|
||||
if (char === "(") {
|
||||
depth++;
|
||||
current += char;
|
||||
continue;
|
||||
}
|
||||
if (char === ")") {
|
||||
depth = Math.max(0, depth - 1);
|
||||
current += char;
|
||||
continue;
|
||||
}
|
||||
if (char === "," && depth === 0) {
|
||||
if (current.trim()) {
|
||||
parts.push(current.trim());
|
||||
}
|
||||
current = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
current += char;
|
||||
}
|
||||
|
||||
if (current.trim()) {
|
||||
parts.push(current.trim());
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
private async writeNativeStaticLayoutTempAsset(
|
||||
bytes: Uint8Array,
|
||||
extension: string,
|
||||
@@ -1901,7 +1963,8 @@ export class ModernVideoExporter {
|
||||
}
|
||||
const background = await this.resolveNativeStaticLayoutBackground();
|
||||
if (!background) {
|
||||
this.nativeStaticLayoutSkipReason = "unsupported-background";
|
||||
this.nativeStaticLayoutSkipReason =
|
||||
this.nativeStaticLayoutBackgroundSkipReason ?? "unsupported-background";
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user