From 31dc6c2defa883335eb02bd3ba31ca5b635eedb7 Mon Sep 17 00:00:00 2001 From: Jakub Bednarski <45859721+Leon135@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:52:47 +0200 Subject: [PATCH] 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 --- src/equicordplugins/gifMaker/index.tsx | 15 ++--- src/equicordplugins/gifMaker/utils/encoder.ts | 25 ++++---- .../gifMaker/utils/gifPicker.ts | 60 ++++++++++++------- 3 files changed, 63 insertions(+), 37 deletions(-) diff --git a/src/equicordplugins/gifMaker/index.tsx b/src/equicordplugins/gifMaker/index.tsx index 96bf53a59..d5bcba70a 100644 --- a/src/equicordplugins/gifMaker/index.tsx +++ b/src/equicordplugins/gifMaker/index.tsx @@ -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(); - 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 => ( - + )); } diff --git a/src/equicordplugins/gifMaker/utils/encoder.ts b/src/equicordplugins/gifMaker/utils/encoder.ts index c7c67434e..b5998e8da 100644 --- a/src/equicordplugins/gifMaker/utils/encoder.ts +++ b/src/equicordplugins/gifMaker/utils/encoder.ts @@ -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); }, diff --git a/src/equicordplugins/gifMaker/utils/gifPicker.ts b/src/equicordplugins/gifMaker/utils/gifPicker.ts index e4bbcbe5d..0418af312 100644 --- a/src/equicordplugins/gifMaker/utils/gifPicker.ts +++ b/src/equicordplugins/gifMaker/utils/gifPicker.ts @@ -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()) { if (!source || depth > 2) return out; @@ -63,12 +49,7 @@ export function collectCandidateUrls(source: unknown, depth = 0, out = new Set= 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; +}