large gifs are converted to vp9 instead of webp

This commit is contained in:
2026-05-17 10:28:19 +02:00
parent 734685b734
commit 838883bd6d
4 changed files with 151 additions and 38 deletions

View File

@@ -7432,6 +7432,10 @@ input#s_avatar {
/* Comments System */
/* Primary definition moved up to line 1082 to avoid overrides */
video.autoplay-gif {
background: rgba(0, 0, 0, 0) !important;
}
#comments-container {
color: var(--white);
font-family: var(--font);
@@ -7615,6 +7619,11 @@ input#s_avatar {
background: #000;
}
.video-embed-wrap:has(.autoplay-gif) {
background: transparent;
box-shadow: none;
}
.video-embed-wrap video {
width: 100%;
display: block;

View File

@@ -504,6 +504,7 @@ class CommentSystem {
contentEl.dataset.raw = this.escapeHtml(fullContent);
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
CommentSystem.autoplayConvertedGifs(contentEl);
} catch (e) {
_f0ckDebug('[CommentSystem] _patchLiveCommentContent failed:', e);
}
@@ -527,6 +528,7 @@ class CommentSystem {
const contentEl = el.querySelector('.comment-content');
if (contentEl) {
contentEl.innerHTML = this.renderCommentContent(data.content, data.comment_id);
CommentSystem.autoplayConvertedGifs(contentEl);
// Flash effect to draw attention
el.classList.remove('new-item-fade');
@@ -1196,6 +1198,7 @@ class CommentSystem {
this.container.innerHTML = html;
this.restoreMediaState(mediaState);
this.syncSubscribeButton(isSubscribed);
CommentSystem.autoplayConvertedGifs(this.container);
// Attach media load listeners to re-stabilize scroll if a hash is active.
// Only during the initial anchor scroll — never on subsequent renders (tab re-focus,
@@ -1307,6 +1310,7 @@ class CommentSystem {
if (contentEl && contentEl.dataset.raw !== incoming.content) {
_f0ckDebug(`[CommentSystem] Reconcile: Updating content for #c${id}`);
contentEl.innerHTML = this.renderCommentContent(incoming.content, incoming.id);
CommentSystem.autoplayConvertedGifs(contentEl);
contentEl.dataset.raw = incoming.content;
}
@@ -1553,7 +1557,7 @@ class CommentSystem {
// Prevents concatenated URLs (url1.webpurl2.webp) being consumed as one giant src.
const safeS = `(?:(?!https?:\\/\\/)\\S)`;
const imageRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/${safeS}+\\.(?:jpg|jpeg|png|gif|webp)(?:\\?${safeS}+)?))(?![\\)\\]])`, 'gi');
const rawVideoRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?))`, 'gi');
const rawVideoRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?(?:#gif)?))`, 'gi');
const rawAudioRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp3|ogg|wav|flac|aac|opus|m4a)(?:\\?[^\\s\\[\\]\\(\\)]+)?))`, 'gi');
const mentionRegex = /(?<!\[)@([a-zA-Z0-9_\-\.]+)(?!\])|\[@([^\]]+)\]/g;
@@ -1666,9 +1670,14 @@ class CommentSystem {
const mediaDomainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${mediaHostsPart})|(?=\\/[a-zA-Z0-9_\\-]))`;
// Video embed: replace anchor links pointing to video files from allowed hosters with a video player
const videoEmbedRegex = new RegExp(`<a\\s[^>]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
const videoEmbedRegex = new RegExp(`<a\\s[^>]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?(?:#gif)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
md = md.replace(videoEmbedRegex, (match, url) => {
return `<span class="video-embed-wrap"><video src="${url}" controls loop muted playsinline preload="metadata"></video></span>`;
const isConvertedGif = url.endsWith('#gif');
const cleanUrl = url.replace(/#gif$/, '');
if (isConvertedGif) {
return `<span class="video-embed-wrap"><video src="${cleanUrl}" class="autoplay-gif" loop muted playsinline preload="auto"></video></span>`;
}
return `<span class="video-embed-wrap"><video src="${cleanUrl}" controls loop muted playsinline preload="metadata"></video></span>`;
});
// Audio embed: replace anchor links pointing to audio files from allowed hosters with an audio player
@@ -1728,6 +1737,23 @@ class CommentSystem {
}
}
/**
* Force-play videos with autoplay attribute in a container.
* Browsers often block autoplay on dynamically inserted elements;
* calling .play() explicitly after DOM insertion resolves this.
*/
static autoplayConvertedGifs(container) {
if (!container) return;
const videos = container.querySelectorAll('video.autoplay-gif');
videos.forEach(v => {
v.autoplay = true;
v.muted = true;
v.play().catch(() => {
v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true });
});
});
}
buildBacklinkMap(comments) {
this.backlinkMap = {};
const process = (c) => {
@@ -2184,7 +2210,7 @@ class CommentSystem {
const json = await res.json();
if (json.success && json.files && json.files.length > 0) {
const fileData = json.files[0];
const url = `/c/${fileData.dest}`;
const url = `/c/${fileData.dest}${fileData.converted_gif ? '#gif' : ''}`;
textarea.value = textarea.value.replace(placeholder, url);
// Update preview with actual thumbnail

View File

@@ -130,7 +130,7 @@
const safeS = `(?:(?!https?:\\/\\/)\\S)`;
const domainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${hostsRegexPart})|(?<!\\S)(?=\\/[a-zA-Z0-9_\\-]))`;
const imageRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/${safeS}+\\.(?:jpg|jpeg|png|gif|webp)(?:\\?${safeS}+)?))(?![\\)\\]])`, 'gi');
const rawVideoRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?))`, 'gi');
const rawVideoRegex = new RegExp(`(?<![\\(\\[])(${domainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]*\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?(?:#gif)?))`, 'gi');
const mentionRegex = /(?<!\[)@([a-zA-Z0-9_\-\.]+)(?!\])|\[@([^\]]+)\]/g;
const renderer = new marked.Renderer();
@@ -303,18 +303,24 @@
const mediaDomainOrRelative = `(?:(?:https?:\\/\\/|\\/\\/)?(?:${mediaHostsPart})|(?=\\/[a-zA-Z0-9_\\-]))`;
// Video label replacement: instead of embedding, show a link
const videoEmbedRegex = new RegExp(`<a\\s[^>]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
const videoEmbedRegex = new RegExp(`<a\\s[^>]*href="(${mediaDomainOrRelative}(?:\\/[^\\s\\[\\]\\(\\)]+\\.(?:mp4|webm|ogv|mov)(?:\\?[^\\s\\[\\]\\(\\)]+)?(?:#gif)?))"[^>]*>([\\s\\S]*?)<\\/a>`, 'gi');
md = md.replace(videoEmbedRegex, (match, url) => {
const isConvertedGif = url.endsWith('#gif');
const cleanUrl = url.replace(/#gif$/, '');
// Converted GIFs → inline autoplay in sidebar too
if (isConvertedGif) {
return `<span class="video-embed-wrap"><video src="${cleanUrl}" class="sidebar-comment-img autoplay-gif" loop muted playsinline preload="auto"></video></span>`;
}
let isSameSite = false;
try {
const urlToParse = url.startsWith('//') ? window.location.protocol + url : url;
const urlToParse = cleanUrl.startsWith('//') ? window.location.protocol + cleanUrl : cleanUrl;
const urlObj = new URL(urlToParse, siteOrigin);
isSameSite = (urlObj.hostname === window.location.hostname);
} catch(e) {
isSameSite = url.startsWith(siteOrigin) || (url.startsWith('/') && !url.startsWith('//'));
isSameSite = cleanUrl.startsWith(siteOrigin) || (cleanUrl.startsWith('/') && !cleanUrl.startsWith('//'));
}
const label = isSameSite ? 'Video Link' : 'External Video Link';
const targetHref = (itemId && commentId) ? `/${itemId}#c${commentId}` : (commentId ? `#sc${commentId}` : url);
const targetHref = (itemId && commentId) ? `/${itemId}#c${commentId}` : (commentId ? `#sc${commentId}` : cleanUrl);
const externalAttr = (itemId && commentId) || commentId ? '' : ' target="_blank" rel="noopener noreferrer"';
return `<a href="${targetHref}"${externalAttr} class="sidebar-video-link"><i class="fa-solid fa-film"></i> ${label} &raquo;</a>`;
});
@@ -482,6 +488,8 @@
</div>`;
}
container.innerHTML = html;
// Auto-play converted GIF videos
container.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }); }); });
};
const renderFromCache = () => {
@@ -500,6 +508,8 @@
}
checkOverflow();
fetchSidebarYoutubeTitles(container);
// Auto-play converted GIF videos
container.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }); }); });
return true;
};
@@ -604,6 +614,8 @@
if (ioSentinel) container.appendChild(ioSentinel);
checkOverflow();
fetchSidebarYoutubeTitles(container);
// Auto-play converted GIF videos
container.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }); }); });
}
} else {
hasMore = false;
@@ -690,6 +702,8 @@
el.classList.add('new-item-fade');
checkOverflow();
fetchSidebarYoutubeTitles(el);
// Auto-play converted GIF videos
inner.querySelectorAll('video.autoplay-gif').forEach(v => { v.autoplay = true; v.muted = true; v.play().catch(() => { v.addEventListener('canplay', () => v.play().catch(() => {}), { once: true }); }); });
}
}
}