This commit is contained in:
1 parent
73d4f6af42
commit
30c76a10ab
4 files changed
+35
-13
No files matched your search
@@ -26,12 +26,14 @@ const { title, description, keywords, openGraph, viewTransition } = Astro.props;
|
|||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
{openGraph && (
|
{openGraph && (
|
||||||
<>
|
<>
|
||||||
<meta name="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:url" content={Astro.url.pathname} />
|
{description && <meta property="og:description" content={description} />}
|
||||||
|
<meta property="og:url" content={Astro.url.href} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:image" content={logo512.src} />
|
<meta property="og:image" content={new URL(logo512.src, Astro.url).href} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<slot name="head" />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
{keywords && <meta name="keywords" content={keywords.join(', ')} />}
|
{keywords && <meta name="keywords" content={keywords.join(', ')} />}
|
||||||
<base href=`${BASE_PATH}/` />
|
<base href=`${BASE_PATH}/` />
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const { title, description, footer = true } = Astro.props;
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={title} description={description} openGraph={true} viewTransition>
|
<BaseLayout title={title} description={description} openGraph={true} viewTransition>
|
||||||
|
{Astro.slots.has('head') && <slot name="head" slot="head" />}
|
||||||
<main class="min-h-[calc(100vh-3.5rem)] h-full w-full relative overflow-y-hidden">
|
<main class="min-h-[calc(100vh-3.5rem)] h-full w-full relative overflow-y-hidden">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -11,19 +11,30 @@ if (!video) return new Response(null, { status: 404 });
|
|||||||
const metadata = await getVideoMetadata(video);
|
const metadata = await getVideoMetadata(video);
|
||||||
if (!metadata) return new Response(null, { status: 404 });
|
if (!metadata) return new Response(null, { status: 404 });
|
||||||
|
|
||||||
const { title, description } = metadata;
|
const { title, description, width, height, duration } = metadata;
|
||||||
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||||
const streamUrl = `${basePath}/video/${encodeURIComponent(video.name)}/stream`;
|
const streamPath = `${basePath}/video/${encodeURIComponent(video.name)}/stream`;
|
||||||
|
const streamUrl = new URL(streamPath, Astro.url);
|
||||||
---
|
---
|
||||||
|
|
||||||
<WebsiteLayout title={`${title} – Video`}>
|
<WebsiteLayout title={`${title} – Video`} description={description ?? undefined}>
|
||||||
|
<Fragment slot="head">
|
||||||
|
<meta property="og:type" content="video" />
|
||||||
|
<meta property="og:video" content={streamUrl.href} />
|
||||||
|
{streamUrl.href.startsWith('https://') && <meta property="og:video:secure_url" content={streamUrl.href} />}
|
||||||
|
<meta property="og:video:type" content={video.contentType} />
|
||||||
|
{width && <meta property="og:video:width" content={width.toString()} />}
|
||||||
|
{height && <meta property="og:video:height" content={height.toString()} />}
|
||||||
|
{duration && <meta property="og:video:duration" content={duration.toString()} />}
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
<section class="mx-auto flex w-full max-w-6xl flex-col gap-5 px-4 py-8 lg:py-12">
|
<section class="mx-auto flex w-full max-w-6xl flex-col gap-5 px-4 py-8 lg:py-12">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm tracking-widest text-base-content/60 uppercase">Video</p>
|
<p class="text-sm tracking-widest text-base-content/60 uppercase">Video</p>
|
||||||
<h1 class="mt-1 text-2xl font-semibold break-words lg:text-4xl">{title}</h1>
|
<h1 class="mt-1 text-2xl font-semibold break-words lg:text-4xl">{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VideoPlayer client:load src={streamUrl} type={video.contentType} title={title} />
|
<VideoPlayer client:load src={streamPath} type={video.contentType} title={title} />
|
||||||
|
|
||||||
{description && (
|
{description && (
|
||||||
<p class="max-w-3xl text-base leading-relaxed whitespace-pre-line text-base-content/70">{description}</p>
|
<p class="max-w-3xl text-base leading-relaxed whitespace-pre-line text-base-content/70">{description}</p>
|
||||||
|
|||||||
+14
-6
@@ -26,13 +26,15 @@ export interface VideoRange {
|
|||||||
export interface VideoMetadata {
|
export interface VideoMetadata {
|
||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
width: number | null;
|
||||||
|
height: number | null;
|
||||||
|
duration: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoMetadataCacheEntry {
|
interface VideoMetadataCacheEntry {
|
||||||
modifiedAtMs: number;
|
modifiedAtMs: number;
|
||||||
size: number;
|
size: number;
|
||||||
title: string;
|
metadata: VideoMetadata;
|
||||||
description: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoMetadataCache = new Map<string, VideoMetadataCacheEntry>();
|
const videoMetadataCache = new Map<string, VideoMetadataCacheEntry>();
|
||||||
@@ -83,22 +85,28 @@ export async function getVideoMetadata(video: ResolvedVideo): Promise<VideoMetad
|
|||||||
const cached = videoMetadataCache.get(video.path);
|
const cached = videoMetadataCache.get(video.path);
|
||||||
|
|
||||||
if (cached && cached.modifiedAtMs === modifiedAtMs && cached.size === video.size) {
|
if (cached && cached.modifiedAtMs === modifiedAtMs && cached.size === video.size) {
|
||||||
return cached;
|
return cached.metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata;
|
let metadata: VideoMetadata;
|
||||||
try {
|
try {
|
||||||
const parsed = await parseFile(video.path, {
|
const parsed = await parseFile(video.path, {
|
||||||
duration: false,
|
duration: false,
|
||||||
skipCovers: true
|
skipCovers: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const videoTrack = parsed.format.trackInfo?.find((track) => track.video);
|
||||||
|
const duration = parsed.format.containerDuration ?? parsed.format.duration;
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
title: parsed.common.title?.trim() || video.name,
|
title: parsed.common.title?.trim() || video.name,
|
||||||
description:
|
description:
|
||||||
parsed.common.longDescription?.trim() ||
|
parsed.common.longDescription?.trim() ||
|
||||||
parsed.common.description?.map((value) => value.trim()).find(Boolean) ||
|
parsed.common.description?.map((value) => value.trim()).find(Boolean) ||
|
||||||
null
|
null,
|
||||||
|
width: videoTrack?.video?.displayWidth ?? videoTrack?.video?.pixelWidth ?? null,
|
||||||
|
height: videoTrack?.video?.displayHeight ?? videoTrack?.video?.pixelHeight ?? null,
|
||||||
|
duration: duration ? Math.round(duration) : null
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
// couldn't read metadata
|
// couldn't read metadata
|
||||||
@@ -108,7 +116,7 @@ export async function getVideoMetadata(video: ResolvedVideo): Promise<VideoMetad
|
|||||||
videoMetadataCache.set(video.path, {
|
videoMetadataCache.set(video.path, {
|
||||||
modifiedAtMs,
|
modifiedAtMs,
|
||||||
size: video.size,
|
size: video.size,
|
||||||
...metadata
|
metadata
|
||||||
});
|
});
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
|
|||||||
Reference in new issue
Block a user