videos #17
@ -18,6 +18,9 @@ sections:
|
|||||||
review_status: "published"
|
review_status: "published"
|
||||||
---
|
---
|
||||||
|
|
||||||
|

|
||||||
|
<video src="https://console.s3.cc24.dev/browser/forensic-pathways/20250610_AllgemeineForensikII_Vorlesung.mp4" controls title="MinIO Video Demo"></video>
|
||||||
|
|
||||||
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
|
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ interface VideoConfig {
|
|||||||
* 3. HTML video tags: <video src="url"></video>
|
* 3. HTML video tags: <video src="url"></video>
|
||||||
* 4. Link syntax with video: [Video Title](url.mp4)
|
* 4. Link syntax with video: [Video Title](url.mp4)
|
||||||
*/
|
*/
|
||||||
|
// REPLACE the transformer body to collect async tasks and call the processor
|
||||||
export const remarkVideoPlugin: Plugin<[VideoConfig?], Root> = (config = {}) => {
|
export const remarkVideoPlugin: Plugin<[VideoConfig?], Root> = (config = {}) => {
|
||||||
const {
|
const {
|
||||||
enableAsync = true,
|
enableAsync = true,
|
||||||
@ -37,84 +38,75 @@ export const remarkVideoPlugin: Plugin<[VideoConfig?], Root> = (config = {}) =>
|
|||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
return async (tree: Root) => {
|
return async (tree: Root) => {
|
||||||
const videoNodes: Array<{ node: any; parent: any; index: number; replacement: any }> = [];
|
const tasks: Array<Promise<void>> = [];
|
||||||
|
|
||||||
// Find video directives (:::video{...})
|
// :::video{...}
|
||||||
visit(tree, 'textDirective', (node: any, index: number | undefined, parent: any) => {
|
visit(tree, 'textDirective', (node: any, index: number | undefined, parent: any) => {
|
||||||
if (node.name === 'video' && typeof index === 'number') {
|
if (node.name === 'video' && typeof index === 'number') {
|
||||||
const videoNode = processVideoDirective(node, defaultOptions);
|
tasks.push((async () => {
|
||||||
if (videoNode) {
|
const replacement = await processVideoDirectiveAsync(node, defaultOptions);
|
||||||
videoNodes.push({ node, parent, index, replacement: videoNode });
|
if (replacement && parent?.children) parent.children[index] = replacement;
|
||||||
}
|
})());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find container directives (:::video ... :::)
|
// :::video ... :::
|
||||||
visit(tree, 'containerDirective', (node: any, index: number | undefined, parent: any) => {
|
visit(tree, 'containerDirective', (node: any, index: number | undefined, parent: any) => {
|
||||||
if (node.name === 'video' && typeof index === 'number') {
|
if (node.name === 'video' && typeof index === 'number') {
|
||||||
const videoNode = processVideoDirective(node, defaultOptions);
|
tasks.push((async () => {
|
||||||
if (videoNode) {
|
const replacement = await processVideoDirectiveAsync(node, defaultOptions);
|
||||||
videoNodes.push({ node, parent, index, replacement: videoNode });
|
if (replacement && parent?.children) parent.children[index] = replacement;
|
||||||
}
|
})());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find image nodes that might be videos
|
// 
|
||||||
visit(tree, 'image', (node: any, index: number | undefined, parent: any) => {
|
visit(tree, 'image', (node: any, index: number | undefined, parent: any) => {
|
||||||
if (isVideoUrl(node.url) && typeof index === 'number') {
|
if (isVideoUrl(node.url) && typeof index === 'number') {
|
||||||
const videoNode = processImageAsVideo(node, defaultOptions);
|
tasks.push((async () => {
|
||||||
if (videoNode) {
|
const replacement = await processImageAsVideoAsync(node, defaultOptions);
|
||||||
videoNodes.push({ node, parent, index, replacement: videoNode });
|
if (replacement && parent?.children) parent.children[index] = replacement;
|
||||||
}
|
})());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find link nodes that point to videos
|
// [Title](video.mp4)
|
||||||
visit(tree, 'link', (node: any, index: number | undefined, parent: any) => {
|
visit(tree, 'link', (node: any, index: number | undefined, parent: any) => {
|
||||||
if (isVideoUrl(node.url) && typeof index === 'number') {
|
if (isVideoUrl(node.url) && typeof index === 'number') {
|
||||||
const videoNode = processLinkAsVideo(node, defaultOptions);
|
tasks.push((async () => {
|
||||||
if (videoNode) {
|
const replacement = await processLinkAsVideoAsync(node, defaultOptions);
|
||||||
videoNodes.push({ node, parent, index, replacement: videoNode });
|
if (replacement && parent?.children) parent.children[index] = replacement;
|
||||||
}
|
})());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process HTML video tags in the tree
|
// Raw <video ...>
|
||||||
visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
|
visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
|
||||||
if (node.value && node.value.includes('<video') && typeof index === 'number') {
|
if (node.value && node.value.includes('<video') && typeof index === 'number') {
|
||||||
const videoNode = processHTMLVideo(node, defaultOptions);
|
tasks.push((async () => {
|
||||||
if (videoNode) {
|
const replacement = await processHTMLVideoAsync(node, defaultOptions);
|
||||||
videoNodes.push({ node, parent, index, replacement: videoNode });
|
if (replacement && parent?.children) parent.children[index] = replacement;
|
||||||
}
|
})());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace all found video nodes
|
await Promise.all(tasks);
|
||||||
for (const { parent, index, replacement } of videoNodes.reverse()) {
|
|
||||||
if (parent && parent.children && Array.isArray(parent.children)) {
|
|
||||||
parent.children[index] = replacement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function processVideoDirective(node: any, defaultOptions: VideoConfig['defaultOptions']): any | null {
|
|
||||||
|
async function processVideoDirectiveAsync(node: any, defaultOptions: VideoConfig['defaultOptions']): Promise<any | null> {
|
||||||
const attributes = node.attributes || {};
|
const attributes = node.attributes || {};
|
||||||
const src = attributes.src;
|
const src = attributes.src;
|
||||||
|
|
||||||
if (!src) {
|
if (!src) {
|
||||||
console.warn('[VIDEO PLUGIN] Video directive missing src attribute');
|
console.warn('[VIDEO PLUGIN] Video directive missing src attribute');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and normalize aspect ratio
|
|
||||||
const aspectRatioValue = attributes.aspectRatio || attributes['aspect-ratio'] || defaultOptions?.aspectRatio;
|
const aspectRatioValue = attributes.aspectRatio || attributes['aspect-ratio'] || defaultOptions?.aspectRatio;
|
||||||
const validAspectRatios = ['16:9', '4:3', '1:1'] as const;
|
const validAspectRatios = ['16:9', '4:3', '1:1'] as const;
|
||||||
const aspectRatio = validAspectRatios.includes(aspectRatioValue as any)
|
const aspectRatio = (validAspectRatios.includes(aspectRatioValue as any) ? aspectRatioValue : '16:9') as '16:9' | '4:3' | '1:1';
|
||||||
? aspectRatioValue as '16:9' | '4:3' | '1:1'
|
|
||||||
: '16:9';
|
|
||||||
|
|
||||||
// Extract options from attributes
|
|
||||||
const options = {
|
const options = {
|
||||||
controls: attributes.controls !== undefined ? attributes.controls !== 'false' : defaultOptions?.controls ?? true,
|
controls: attributes.controls !== undefined ? attributes.controls !== 'false' : defaultOptions?.controls ?? true,
|
||||||
autoplay: attributes.autoplay !== undefined ? attributes.autoplay !== 'false' : defaultOptions?.autoplay ?? false,
|
autoplay: attributes.autoplay !== undefined ? attributes.autoplay !== 'false' : defaultOptions?.autoplay ?? false,
|
||||||
@ -131,87 +123,68 @@ function processVideoDirective(node: any, defaultOptions: VideoConfig['defaultOp
|
|||||||
poster: attributes.poster
|
poster: attributes.poster
|
||||||
};
|
};
|
||||||
|
|
||||||
return createVideoElement(src, metadata, options);
|
const html = await buildProcessedVideoHTML(src, metadata, options);
|
||||||
|
return { type: 'html', value: html };
|
||||||
}
|
}
|
||||||
|
|
||||||
function processImageAsVideo(node: any, defaultOptions: VideoConfig['defaultOptions']): any | null {
|
async function processImageAsVideoAsync(node: any, defaultOptions: VideoConfig['defaultOptions']): Promise<any | null> {
|
||||||
const src = node.url;
|
const src = node.url;
|
||||||
const metadata = {
|
const metadata = { title: node.title || node.alt, description: node.alt };
|
||||||
title: node.title || node.alt,
|
|
||||||
description: node.alt
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure aspectRatio is properly typed
|
|
||||||
const options = {
|
const options = {
|
||||||
controls: defaultOptions?.controls ?? true,
|
controls: defaultOptions?.controls ?? true,
|
||||||
autoplay: defaultOptions?.autoplay ?? false,
|
autoplay: defaultOptions?.autoplay ?? false,
|
||||||
muted: defaultOptions?.muted ?? false,
|
muted: defaultOptions?.muted ?? false,
|
||||||
loop: false,
|
loop: false,
|
||||||
preload: 'metadata' as const,
|
preload: 'metadata' as const,
|
||||||
aspectRatio: defaultOptions?.aspectRatio ?? '16:9' as const,
|
aspectRatio: (defaultOptions?.aspectRatio ?? '16:9') as '16:9' | '4:3' | '1:1',
|
||||||
showMetadata: defaultOptions?.showMetadata ?? true
|
showMetadata: defaultOptions?.showMetadata ?? true
|
||||||
};
|
};
|
||||||
|
const html = await buildProcessedVideoHTML(src, metadata, options);
|
||||||
return createVideoElement(src, metadata, options);
|
return { type: 'html', value: html };
|
||||||
}
|
}
|
||||||
|
|
||||||
function processLinkAsVideo(node: any, defaultOptions: VideoConfig['defaultOptions']): any | null {
|
|
||||||
const src = node.url;
|
|
||||||
const metadata = {
|
|
||||||
title: node.title || extractTextContent(node),
|
|
||||||
description: node.title
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure aspectRatio is properly typed
|
async function processLinkAsVideoAsync(node: any, defaultOptions: VideoConfig['defaultOptions']): Promise<any | null> {
|
||||||
|
const src = node.url;
|
||||||
|
const metadata = { title: node.title || extractTextContent(node), description: node.title };
|
||||||
const options = {
|
const options = {
|
||||||
controls: defaultOptions?.controls ?? true,
|
controls: defaultOptions?.controls ?? true,
|
||||||
autoplay: defaultOptions?.autoplay ?? false,
|
autoplay: defaultOptions?.autoplay ?? false,
|
||||||
muted: defaultOptions?.muted ?? false,
|
muted: defaultOptions?.muted ?? false,
|
||||||
loop: false,
|
loop: false,
|
||||||
preload: 'metadata' as const,
|
preload: 'metadata' as const,
|
||||||
aspectRatio: defaultOptions?.aspectRatio ?? '16:9' as const,
|
aspectRatio: (defaultOptions?.aspectRatio ?? '16:9') as '16:9' | '4:3' | '1:1',
|
||||||
|
showMetadata: defaultOptions?.showMetadata ?? true
|
||||||
|
};
|
||||||
|
const html = await buildProcessedVideoHTML(src, metadata, options);
|
||||||
|
return { type: 'html', value: html };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processHTMLVideoAsync(node: any, defaultOptions: VideoConfig['defaultOptions']): Promise<any | null> {
|
||||||
|
const htmlContent = node.value || '';
|
||||||
|
const srcMatch = htmlContent.match(/src=["']([^"']+)["']/);
|
||||||
|
if (!srcMatch) return null;
|
||||||
|
|
||||||
|
const titleMatch = htmlContent.match(/title=["']([^"']+)["']/);
|
||||||
|
const posterMatch = htmlContent.match(/poster=["']([^"']+)["']/);
|
||||||
|
|
||||||
|
const src = srcMatch[1];
|
||||||
|
const metadata = { title: titleMatch?.[1], poster: posterMatch?.[1] };
|
||||||
|
const options = {
|
||||||
|
controls: htmlContent.includes('controls'),
|
||||||
|
autoplay: htmlContent.includes('autoplay'),
|
||||||
|
muted: htmlContent.includes('muted'),
|
||||||
|
loop: htmlContent.includes('loop'),
|
||||||
|
preload: 'metadata' as const,
|
||||||
|
aspectRatio: (defaultOptions?.aspectRatio ?? '16:9') as '16:9' | '4:3' | '1:1',
|
||||||
showMetadata: defaultOptions?.showMetadata ?? true
|
showMetadata: defaultOptions?.showMetadata ?? true
|
||||||
};
|
};
|
||||||
|
|
||||||
return createVideoElement(src, metadata, options);
|
const html = await buildProcessedVideoHTML(src, metadata, options);
|
||||||
|
return { type: 'html', value: html };
|
||||||
}
|
}
|
||||||
|
|
||||||
function processHTMLVideo(node: any, defaultOptions: VideoConfig['defaultOptions']): any | null {
|
function buildProcessedVideoHTML(src: string, metadata: any, options: {
|
||||||
try {
|
|
||||||
// Parse HTML to extract video attributes
|
|
||||||
const htmlContent = node.value;
|
|
||||||
const srcMatch = htmlContent.match(/src=["']([^"']+)["']/);
|
|
||||||
const titleMatch = htmlContent.match(/title=["']([^"']+)["']/);
|
|
||||||
const posterMatch = htmlContent.match(/poster=["']([^"']+)["']/);
|
|
||||||
|
|
||||||
if (!srcMatch) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const src = srcMatch[1];
|
|
||||||
const metadata = {
|
|
||||||
title: titleMatch?.[1],
|
|
||||||
poster: posterMatch?.[1]
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
controls: htmlContent.includes('controls'),
|
|
||||||
autoplay: htmlContent.includes('autoplay'),
|
|
||||||
muted: htmlContent.includes('muted'),
|
|
||||||
loop: htmlContent.includes('loop'),
|
|
||||||
preload: 'metadata' as const,
|
|
||||||
aspectRatio: defaultOptions?.aspectRatio ?? '16:9' as const,
|
|
||||||
showMetadata: defaultOptions?.showMetadata ?? true
|
|
||||||
};
|
|
||||||
|
|
||||||
return createVideoElement(src, metadata, options);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[VIDEO PLUGIN] Failed to process HTML video:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createVideoElement(src: string, metadata: any, options: {
|
|
||||||
controls: boolean;
|
controls: boolean;
|
||||||
autoplay: boolean;
|
autoplay: boolean;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
|
@ -147,37 +147,42 @@ export class VideoProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// identifyVideoSource(url: string)
|
||||||
private identifyVideoSource(url: string): { type: VideoSource['type']; url: string } {
|
private identifyVideoSource(url: string): { type: VideoSource['type']; url: string } {
|
||||||
console.log(`[VIDEO PROCESSOR] Identifying source for URL: ${url}`);
|
console.log(`[VIDEO PROCESSOR] Identifying source for URL: ${url}`);
|
||||||
|
|
||||||
// Nextcloud share links
|
// Treat links that point at your configured MinIO endpoint as 'minio'
|
||||||
|
try {
|
||||||
|
const minioHost = process.env.MINIO_URL ? new URL(process.env.MINIO_URL).host : null;
|
||||||
|
const urlHost = new URL(url).host;
|
||||||
|
if (minioHost && urlHost === minioHost) {
|
||||||
|
console.log(`[VIDEO PROCESSOR] Detected MinIO by endpoint host match: ${minioHost}`);
|
||||||
|
return { type: 'minio', url };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore URL parse errors */
|
||||||
|
}
|
||||||
|
|
||||||
if (url.includes('/s/') || url.includes('nextcloud') || url.includes('owncloud')) {
|
if (url.includes('/s/') || url.includes('nextcloud') || url.includes('owncloud')) {
|
||||||
console.log(`[VIDEO PROCESSOR] Detected Nextcloud share link`);
|
console.log(`[VIDEO PROCESSOR] Detected Nextcloud share link`);
|
||||||
return { type: 'nextcloud', url };
|
return { type: 'nextcloud', url };
|
||||||
}
|
}
|
||||||
|
|
||||||
// S3 URLs
|
|
||||||
if (url.includes('amazonaws.com') || url.includes('.s3.')) {
|
if (url.includes('amazonaws.com') || url.includes('.s3.')) {
|
||||||
console.log(`[VIDEO PROCESSOR] Detected S3 URL`);
|
console.log(`[VIDEO PROCESSOR] Detected S3 URL`);
|
||||||
return { type: 's3', url };
|
return { type: 's3', url };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minio URLs (common patterns)
|
|
||||||
if (url.includes('minio') || url.match(/:\d+\/[^\/]+\/.*\.(mp4|webm|ogg|mov|avi)/i)) {
|
if (url.includes('minio') || url.match(/:\d+\/[^\/]+\/.*\.(mp4|webm|ogg|mov|avi)/i)) {
|
||||||
console.log(`[VIDEO PROCESSOR] Detected Minio URL`);
|
console.log(`[VIDEO PROCESSOR] Detected Minio URL`);
|
||||||
return { type: 'minio', url };
|
return { type: 'minio', url };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local files (relative paths)
|
|
||||||
if (url.startsWith('/') && !url.startsWith('http')) {
|
if (url.startsWith('/') && !url.startsWith('http')) {
|
||||||
console.log(`[VIDEO PROCESSOR] Detected local file path`);
|
console.log(`[VIDEO PROCESSOR] Detected local file path`);
|
||||||
return { type: 'local', url };
|
return { type: 'local', url };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct HTTP(S) URLs
|
|
||||||
console.log(`[VIDEO PROCESSOR] Detected direct HTTP URL`);
|
console.log(`[VIDEO PROCESSOR] Detected direct HTTP URL`);
|
||||||
return { type: 'direct', url };
|
return { type: 'direct', url };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async processNextcloudVideo(url: string): Promise<VideoSource[]> {
|
private async processNextcloudVideo(url: string): Promise<VideoSource[]> {
|
||||||
console.log(`[VIDEO PROCESSOR] Processing Nextcloud video: ${url}`);
|
console.log(`[VIDEO PROCESSOR] Processing Nextcloud video: ${url}`);
|
||||||
@ -262,7 +267,7 @@ export class VideoProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace processS3MinioVideo() with:
|
// Strengthen the MinIO pre-signing to force a clean, streamable response
|
||||||
private async processS3MinioVideo(url: string, type: 's3' | 'minio'): Promise<VideoSource[]> {
|
private async processS3MinioVideo(url: string, type: 's3' | 'minio'): Promise<VideoSource[]> {
|
||||||
console.log(`[VIDEO PROCESSOR] Processing ${type} video: ${url}`);
|
console.log(`[VIDEO PROCESSOR] Processing ${type} video: ${url}`);
|
||||||
|
|
||||||
@ -270,18 +275,25 @@ export class VideoProcessor {
|
|||||||
const parsed = this.normalizeMinioUrl(url);
|
const parsed = this.normalizeMinioUrl(url);
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
try {
|
try {
|
||||||
const cmd = new GetObjectCommand({ Bucket: parsed.bucket, Key: parsed.key });
|
const mime = this.getMimeType(url);
|
||||||
const signed = await getSignedUrl(this.s3Client, cmd, { expiresIn: 3600 }); // 1h
|
const cmd = new GetObjectCommand({
|
||||||
|
Bucket: parsed.bucket,
|
||||||
|
Key: parsed.key,
|
||||||
|
ResponseContentType: mime, // ensure video/* MIME
|
||||||
|
ResponseContentDisposition: 'inline', // avoid download
|
||||||
|
ResponseCacheControl: 'public, max-age=3600' // optional, helps caching
|
||||||
|
});
|
||||||
|
const signed = await getSignedUrl(this.s3Client, cmd, { expiresIn: 3600 });
|
||||||
return [{ type: 'minio', url: signed, originalUrl: url, cached: false }];
|
return [{ type: 'minio', url: signed, originalUrl: url, cached: false }];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[VIDEO PROCESSOR] MinIO pre-sign failed:', e);
|
console.warn('[VIDEO PROCESSOR] MinIO pre-sign failed:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [{ type, url, cached: false }];
|
return [{ type, url, cached: false }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private normalizeMinioUrl(inputUrl: string): { bucket: string; key: string } | null {
|
private normalizeMinioUrl(inputUrl: string): { bucket: string; key: string } | null {
|
||||||
try {
|
try {
|
||||||
const u = new URL(inputUrl);
|
const u = new URL(inputUrl);
|
||||||
@ -580,16 +592,27 @@ export class VideoProcessor {
|
|||||||
|
|
||||||
console.log(`[VIDEO PROCESSOR] Primary source: ${primarySource.type} (cached: ${primarySource.cached})`);
|
console.log(`[VIDEO PROCESSOR] Primary source: ${primarySource.type} (cached: ${primarySource.cached})`);
|
||||||
console.log(`[VIDEO PROCESSOR] Primary source URL: ${primarySource.url}`);
|
console.log(`[VIDEO PROCESSOR] Primary source URL: ${primarySource.url}`);
|
||||||
|
|
||||||
|
const needsCrossOrigin = processedVideo.sources.some(s =>
|
||||||
|
/^https?:\/\//i.test(s.url) && !this.isSameOrigin(s.url)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (needsCrossOrigin==true){
|
||||||
|
console.log('FUCK CROSS ORIGIN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// In generateVideoHTML(...), when building videoAttributes array
|
||||||
let videoAttributes = [
|
let videoAttributes = [
|
||||||
controls ? 'controls' : '',
|
controls ? 'controls' : '',
|
||||||
autoplay ? 'autoplay' : '',
|
autoplay ? 'autoplay' : '',
|
||||||
muted ? 'muted' : '',
|
muted ? 'muted' : '',
|
||||||
loop ? 'loop' : '',
|
loop ? 'loop' : '',
|
||||||
|
needsCrossOrigin ? 'crossorigin="anonymous"' : '',
|
||||||
`preload="${preload}"`,
|
`preload="${preload}"`,
|
||||||
metadata.poster ? `poster="${metadata.poster}"` : '',
|
metadata.poster ? `poster="${metadata.poster}"` : '',
|
||||||
`data-video-title="${metadata.title || 'Embedded Video'}"`
|
`data-video-title="${metadata.title || 'Embedded Video'}"`
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
|
||||||
console.log(`[VIDEO PROCESSOR] Video attributes: ${videoAttributes}`);
|
console.log(`[VIDEO PROCESSOR] Video attributes: ${videoAttributes}`);
|
||||||
|
|
||||||
@ -626,24 +649,40 @@ export class VideoProcessor {
|
|||||||
|
|
||||||
return finalHTML;
|
return finalHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMimeType(url: string): string {
|
private isSameOrigin(u: string): boolean {
|
||||||
const extension = url.split('.').pop()?.toLowerCase();
|
try {
|
||||||
const mimeTypes: Record<string, string> = {
|
const site = process.env.PUBLIC_SITE_ORIGIN || '';
|
||||||
'mp4': 'video/mp4',
|
if (!site) return false;
|
||||||
'webm': 'video/webm',
|
return new URL(u).host === new URL(site).host;
|
||||||
'ogg': 'video/ogg',
|
} catch {
|
||||||
'mov': 'video/quicktime',
|
return false;
|
||||||
'avi': 'video/x-msvideo',
|
}
|
||||||
'm4v': 'video/m4v'
|
|
||||||
};
|
|
||||||
|
|
||||||
const mimeType = mimeTypes[extension || ''] || 'video/mp4';
|
|
||||||
console.log(`[VIDEO PROCESSOR] MIME type for extension '${extension}': ${mimeType}`);
|
|
||||||
|
|
||||||
return mimeType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make getMimeType() robust against presigned URLs with query strings
|
||||||
|
private getMimeType(url: string): string {
|
||||||
|
let extension: string | undefined;
|
||||||
|
try {
|
||||||
|
const pathname = new URL(url).pathname;
|
||||||
|
extension = pathname.split('.').pop()?.toLowerCase();
|
||||||
|
} catch {
|
||||||
|
extension = url.split('?')[0].split('.').pop()?.toLowerCase();
|
||||||
|
}
|
||||||
|
const mimeTypes: Record<string, string> = {
|
||||||
|
mp4: 'video/mp4',
|
||||||
|
webm: 'video/webm',
|
||||||
|
ogg: 'video/ogg',
|
||||||
|
mov: 'video/quicktime',
|
||||||
|
avi: 'video/x-msvideo',
|
||||||
|
m4v: 'video/m4v'
|
||||||
|
};
|
||||||
|
const mimeType = (extension && mimeTypes[extension]) ? mimeTypes[extension] : 'video/mp4';
|
||||||
|
console.log(`[VIDEO PROCESSOR] MIME type for extension '${extension}': ${mimeType}`);
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private formatDuration(seconds: number): string {
|
private formatDuration(seconds: number): string {
|
||||||
const hours = Math.floor(seconds / 3600);
|
const hours = Math.floor(seconds / 3600);
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user