fix: resolve video playback issue in publications using view endpoint and legacy URL normalization

This commit is contained in:
Carlos (Almeira) 2026-08-20 09:50:14 -05:00
parent 5c15a34a19
commit 3dff30ab73
4 changed files with 28 additions and 13 deletions

View File

@ -10,12 +10,16 @@ import { Metadata, ResolvingMetadata } from 'next';
function formatTextToHtml(text: string) {
if (!text) return '';
// Normalize any video preview URLs to view URLs for legacy content
const normalizedText = text
.replace(/(<video[^>]*src=["'])([^"']*)\/preview\?([^"']*["'])/gi, '$1$2/view?$3')
.replace(/(<source[^>]*src=["'])([^"']*)\/preview\?([^"']*["'])/gi, '$1$2/view?$3');
// Si ya contiene etiquetas estructurales de HTML (párrafos, saltos, divs, listas), lo devolvemos tal cual
if (/<\/?(?:p|br|div|h[1-6]|ul|ol|li|table)[^>]*>/i.test(text)) {
return text;
if (/<\/?(?:p|br|div|h[1-6]|ul|ol|li|table)[^>]*>/i.test(normalizedText)) {
return normalizedText;
}
// Convertimos texto plano con saltos de línea a párrafos y <br/>, permitiendo que etiquetas inline como <strong> o <iframe> se mantengan.
return text.split(/\n\s*\n/)
return normalizedText.split(/\n\s*\n/)
.filter(p => p.trim() !== '')
.map(p => `<p>${p.replace(/\n/g, '<br />')}</p>`)
.join('');

View File

@ -8,12 +8,16 @@ import { z } from 'zod';
import DOMPurify from 'isomorphic-dompurify';
// Re-usable rich text validator
const safeHtmlSchema = z.string().transform(html =>
DOMPurify.sanitize(html, {
ADD_TAGS: ['iframe'],
ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'style', 'class']
})
);
const safeHtmlSchema = z.string().transform(html => {
const normalizedHtml = html
.replace(/(<video[^>]*src=["'])([^"']*)\/preview\?([^"']*["'])/gi, '$1$2/view?$3')
.replace(/(<source[^>]*src=["'])([^"']*)\/preview\?([^"']*["'])/gi, '$1$2/view?$3');
return DOMPurify.sanitize(normalizedHtml, {
ADD_TAGS: ['iframe', 'video', 'source'],
ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'style', 'class', 'controls', 'src', 'type', 'muted', 'loop', 'autoplay', 'preload']
});
});
/**
* Extrae y limpia los datos del FormData usando el SCHEMA_MAP como whitelist.

View File

@ -11,7 +11,7 @@ export function getAppwriteImageUrl(fileId: string, bucketId?: string, width = 4
if (!fileId) return '';
const finalBucketId = bucketId || APPWRITE_ALMEIRA_CONFIG.mediaBucketId;
if (isVideo) {
return `${endpoint}/storage/buckets/${finalBucketId}/files/${fileId}/preview?project=${projectId}`;
return `${endpoint}/storage/buckets/${finalBucketId}/files/${fileId}/view?project=${projectId}`;
}
return `${endpoint}/storage/buckets/${finalBucketId}/files/${fileId}/preview?project=${projectId}&width=${width}&height=${height}&quality=80`;
}

View File

@ -46,6 +46,9 @@ const VideoNode = Node.create({
},
renderHTML({ HTMLAttributes }) {
if (HTMLAttributes.src && typeof HTMLAttributes.src === 'string') {
HTMLAttributes.src = HTMLAttributes.src.replace('/preview?', '/view?');
}
return ['video', mergeAttributes(HTMLAttributes, { class: 'w-full aspect-video rounded-xl shadow-lg my-6', controls: 'true', preload: 'metadata' })]
},
});
@ -76,7 +79,8 @@ const MenuBar = ({ editor }: { editor: any }) => {
const handleMediaInsert = (fileId: string, url?: string, tipo?: string) => {
if (url) {
if (tipo === 'video') {
editor.chain().focus().insertContent(`<video src="${url}" controls="true" preload="metadata" class="w-full aspect-video rounded-xl shadow-lg my-6"></video>`).run();
const videoUrl = url.replace('/preview?', '/view?');
editor.chain().focus().insertContent(`<video src="${videoUrl}" controls="true" preload="metadata" class="w-full aspect-video rounded-xl shadow-lg my-6"></video>`).run();
} else {
editor.chain().focus().setImage({ src: url }).run();
}
@ -300,8 +304,11 @@ export const RichTextEditor = ({ value, onChange, label }: RichTextEditorProps)
// Keep content synced if value changes externally
useEffect(() => {
if (editor && value !== editor.getHTML()) {
editor.commands.setContent(value);
if (editor && value && value !== editor.getHTML()) {
const fixedValue = value
.replace(/(<video[^>]*src=["'])([^"']*)\/preview\?([^"']*["'])/gi, '$1$2/view?$3')
.replace(/(<source[^>]*src=["'])([^"']*)\/preview\?([^"']*["'])/gi, '$1$2/view?$3');
editor.commands.setContent(fixedValue);
}
}, [value, editor]);