fix(gifMaker): fix discord cdn and gif/webp/mp4 madness (#1185)
* fix(gifMaker): fix discord cdn and gif/webp/mp4 madness * fix(gifMaker): code review * refr(gifMaker): apply murphy's fix for gif url handling, increased internal fps to 30 from 10
This commit is contained in:
parent
1f1b0536b3
commit
31dc6c2def
3 changed files with 63 additions and 37 deletions
|
|
@ -23,7 +23,7 @@ import css from "./styles.css?managed";
|
|||
import { DEFAULT_OPTIONS, type GifMakerOptions, type GoogleFontMetadata } from "./types";
|
||||
import { clamp, getInitialSize, getMediaInfo } from "./utils/contextMenu";
|
||||
import { cleanupBlobUrl, createGif, loadImage, loadVideo } from "./utils/encoder";
|
||||
import { applyTenorMp4Fix, collectCandidateUrls, isLikelyVideoUrl, normalizeUrl, orderCandidateUrls } from "./utils/gifPicker";
|
||||
import { collectCandidateUrls, ensureGifUrl, isLikelyVideoUrl, normalizeUrl, orderCandidateUrls, stripDiscordFormatParam } from "./utils/gifPicker";
|
||||
|
||||
const cl = classNameFactory("vc-gifmaker-");
|
||||
const logger = new Logger("gifMaker");
|
||||
|
|
@ -457,20 +457,21 @@ function openGifMakerFromItem(item) {
|
|||
|
||||
const candidates = collectCandidateUrls(item);
|
||||
const adjustedCandidates = new Set<string>();
|
||||
if (directUrl) adjustedCandidates.add(applyTenorMp4Fix(normalizeUrl(directUrl)));
|
||||
if (directUrl) adjustedCandidates.add(normalizeUrl(directUrl));
|
||||
for (const candidate of candidates) {
|
||||
adjustedCandidates.add(applyTenorMp4Fix(candidate));
|
||||
adjustedCandidates.add(candidate);
|
||||
}
|
||||
|
||||
const preferredUrl = directUrl ? applyTenorMp4Fix(normalizeUrl(directUrl)) : null;
|
||||
const preferredUrl = directUrl ? normalizeUrl(directUrl) : null;
|
||||
const orderedUrls = orderCandidateUrls(preferredUrl, adjustedCandidates);
|
||||
if (!orderedUrls.length) return;
|
||||
|
||||
const firstUrl = orderedUrls[0];
|
||||
const isVideo = isLikelyVideoUrl(firstUrl);
|
||||
const firstUrl = ensureGifUrl(orderedUrls[0]);
|
||||
const cleanedUrl = stripDiscordFormatParam(firstUrl);
|
||||
const isVideo = isLikelyVideoUrl(cleanedUrl);
|
||||
|
||||
openModal(modalProps => (
|
||||
<GifMakerModal url={firstUrl} isVideo={isVideo} {...modalProps} />
|
||||
<GifMakerModal url={cleanedUrl} isVideo={isVideo} {...modalProps} />
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { sleep } from "@utils/misc";
|
||||
import type { PluginNative } from "@utils/types";
|
||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||
import { decompressFrames, parseGIF } from "gifuct-js";
|
||||
|
|
@ -12,9 +13,8 @@ import { CAPTIONS } from "../captions";
|
|||
import { measureTextLines } from "../captions/caption";
|
||||
import type { GifMakerOptions } from "../types";
|
||||
|
||||
const MAX_FRAMES = 50;
|
||||
const INTERNAL_FPS = 10;
|
||||
const INTERNAL_MAX_DURATION = 3;
|
||||
const MAX_FRAMES = 200;
|
||||
const INTERNAL_FPS = 30;
|
||||
const PALETTE_COLORS = 255;
|
||||
const MAX_GIF_SCAN_BYTES = 524288; // 512KB
|
||||
|
||||
|
|
@ -238,18 +238,19 @@ async function createGifFromVideo(url: string, options: GifMakerOptions): Promis
|
|||
try {
|
||||
const { duration } = video;
|
||||
const frameCount = Math.min(
|
||||
INTERNAL_FPS * INTERNAL_MAX_DURATION,
|
||||
Math.floor(duration * INTERNAL_FPS),
|
||||
MAX_FRAMES
|
||||
);
|
||||
|
||||
const interval = duration / frameCount;
|
||||
const delay = Math.round(interval * 1000);
|
||||
const delays = new Array(frameCount).fill(delay);
|
||||
|
||||
return await encodeFrames(options.width, options.height, options, frameCount, async (ctx, i) => {
|
||||
video.currentTime = i * interval;
|
||||
await waitForSeek(video);
|
||||
ctx.drawImage(video, 0, 0, options.width, options.height);
|
||||
});
|
||||
}, delays);
|
||||
} finally {
|
||||
cleanupBlobUrl(video);
|
||||
}
|
||||
|
|
@ -486,13 +487,13 @@ async function createGifFromAnimatedImage(url: string, options: GifMakerOptions)
|
|||
|
||||
const patchCanvas = document.createElement("canvas");
|
||||
|
||||
const frameCount = Math.min(frames.length, MAX_FRAMES);
|
||||
const delays: number[] = [];
|
||||
const totalFrames = frames.length;
|
||||
const rendered: HTMLCanvasElement[] = [];
|
||||
const delays: number[] = [];
|
||||
|
||||
for (let i = 0; i < frameCount; i++) {
|
||||
for (let i = 0; i < totalFrames; i++) {
|
||||
const frame = frames[i];
|
||||
delays.push(frame.delay * 10);
|
||||
delays.push(frame.delay);
|
||||
|
||||
if (i > 0) {
|
||||
const prev = frames[i - 1];
|
||||
|
|
@ -526,10 +527,14 @@ async function createGifFromAnimatedImage(url: string, options: GifMakerOptions)
|
|||
if (!snapCtx) throw new Error("Failed to get canvas context for frame snapshot.");
|
||||
snapCtx.drawImage(composite, 0, 0);
|
||||
rendered.push(snap);
|
||||
|
||||
if (i % 20 === 19) {
|
||||
await sleep(0);
|
||||
}
|
||||
}
|
||||
|
||||
return await encodeFrames(
|
||||
options.width, options.height, options, frameCount,
|
||||
options.width, options.height, options, totalFrames,
|
||||
(encodeCtx, i) => {
|
||||
encodeCtx.drawImage(rendered[i], 0, 0, options.width, options.height);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,20 +15,6 @@ export function looksLikeUrl(value: string) {
|
|||
return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("//");
|
||||
}
|
||||
|
||||
export function applyTenorMp4Fix(url: string) {
|
||||
try {
|
||||
const { host } = new URL(url);
|
||||
if (!host.endsWith("tenor.com")) return url;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
|
||||
const typeIndex = url.lastIndexOf("/") - 1;
|
||||
if (typeIndex <= 0 || url[typeIndex] === "o") return url;
|
||||
|
||||
return url.slice(0, typeIndex) + "o" + url.slice(typeIndex + 1);
|
||||
}
|
||||
|
||||
export function collectCandidateUrls(source: unknown, depth = 0, out = new Set<string>()) {
|
||||
if (!source || depth > 2) return out;
|
||||
|
||||
|
|
@ -63,12 +49,7 @@ export function collectCandidateUrls(source: unknown, depth = 0, out = new Set<s
|
|||
}
|
||||
|
||||
export function scoreUrl(url: string) {
|
||||
let host = "";
|
||||
try {
|
||||
host = new URL(url).host;
|
||||
} catch {
|
||||
// invalid URL, host remains empty
|
||||
}
|
||||
const { host } = new URL(url);
|
||||
|
||||
let score = 0;
|
||||
if (host.endsWith("discordapp.net") || host.endsWith("discordapp.com")) score += 100;
|
||||
|
|
@ -94,3 +75,42 @@ export function orderCandidateUrls(preferred: string | null, candidates: Set<str
|
|||
export function isLikelyVideoUrl(url: string) {
|
||||
return /\.(webm|mp4|m4v)(\?|$)/i.test(url);
|
||||
}
|
||||
|
||||
export function ensureGifUrl(url: string): string {
|
||||
if (/\.gif(\?|$)/i.test(url)) return url;
|
||||
|
||||
const parsed = new URL(url);
|
||||
const host = parsed.hostname;
|
||||
|
||||
if (host.includes("tenor.com")) {
|
||||
const paths = parsed.pathname.split("/").filter(Boolean);
|
||||
if (paths.length >= 2) {
|
||||
const idSegment = paths[paths.length - 2];
|
||||
paths[paths.length - 2] = idSegment.replace(/Po$/, "Ad");
|
||||
paths[paths.length - 1] = "tenor.gif";
|
||||
parsed.pathname = "/" + paths.join("/");
|
||||
}
|
||||
parsed.search = "";
|
||||
return parsed.href;
|
||||
}
|
||||
|
||||
if (host.includes("giphy.com")) {
|
||||
const paths = parsed.pathname.split("/").filter(Boolean);
|
||||
if (paths.length >= 2) {
|
||||
const gifId = paths[paths.length - 2];
|
||||
return `https://i.giphy.com/${gifId}.gif`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function stripDiscordFormatParam(url: string): string {
|
||||
const parsed = new URL(url);
|
||||
if (/discord(app)?\.(com|net|gg)/i.test(parsed.hostname)) {
|
||||
parsed.searchParams.delete("format");
|
||||
parsed.searchParams.delete("animated");
|
||||
}
|
||||
return parsed.href;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue