feat: add frame caching demo documentation

This commit is contained in:
zhangheng
2026-05-19 16:19:55 +08:00
commit 3ffeb1ae62
18 changed files with 4002 additions and 0 deletions

461
web/app/globals.css Normal file
View File

@@ -0,0 +1,461 @@
:root {
color-scheme: dark;
--bg: #07101d;
--bg-soft: #0d1828;
--panel: rgba(12, 19, 33, 0.82);
--panel-strong: rgba(16, 25, 41, 0.94);
--border: rgba(255, 255, 255, 0.1);
--text: #eef4ff;
--muted: #a9bbd5;
--accent: #70d6ff;
--accent-strong: #35b8ff;
--accent-warm: #f4b860;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
}
* {
box-sizing: border-box;
}
html,
body {
min-height: 100%;
margin: 0;
}
body {
color: var(--text);
background:
radial-gradient(circle at top left, rgba(112, 214, 255, 0.18), transparent 32%),
radial-gradient(circle at 80% 18%, rgba(244, 184, 96, 0.12), transparent 28%),
linear-gradient(180deg, #07101d 0%, #040912 100%);
font-family: var(--font-body), "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
button,
input {
font: inherit;
}
a {
color: inherit;
}
.viewer-shell {
position: relative;
min-height: 100vh;
padding: 32px;
overflow: hidden;
}
.viewer-shell::before,
.viewer-shell::after {
content: "";
position: absolute;
inset: auto;
width: 420px;
height: 420px;
border-radius: 999px;
filter: blur(72px);
opacity: 0.32;
pointer-events: none;
}
.viewer-shell::before {
top: -100px;
right: -120px;
background: rgba(112, 214, 255, 0.24);
}
.viewer-shell::after {
bottom: -120px;
left: -120px;
background: rgba(244, 184, 96, 0.18);
}
.viewer-inner {
position: relative;
z-index: 1;
max-width: 1440px;
margin: 0 auto;
display: grid;
gap: 20px;
}
.hero-card,
.panel-card {
border: 1px solid var(--border);
background: var(--panel);
box-shadow: var(--shadow);
backdrop-filter: blur(16px);
}
.hero-card {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.85fr);
gap: 20px;
padding: 28px;
border-radius: 28px;
}
.eyebrow {
margin: 0 0 12px;
color: var(--accent);
font-family: var(--font-mono), monospace;
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.hero-title {
margin: 0;
font-family: var(--font-display), var(--font-body), sans-serif;
font-size: clamp(2rem, 4vw, 4rem);
line-height: 1.02;
}
.hero-copy p {
margin: 14px 0 0;
max-width: 68ch;
color: var(--muted);
line-height: 1.7;
}
.hero-metrics {
display: grid;
gap: 12px;
align-content: start;
}
.metric {
padding: 16px 18px;
border-radius: 20px;
background: var(--panel-strong);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.metric-label {
margin: 0 0 8px;
color: var(--muted);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.metric-value {
margin: 0;
font-family: var(--font-mono), monospace;
font-size: 16px;
line-height: 1.45;
overflow-wrap: anywhere;
word-break: break-word;
}
.metric-note {
margin: 8px 0 0;
color: var(--muted);
font-size: 13px;
line-height: 1.6;
}
.panel-card {
padding: 22px;
border-radius: 24px;
}
.toolbar {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 14px;
align-items: end;
}
.toolbar-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-end;
align-items: center;
}
.field {
display: grid;
gap: 10px;
}
.field-label,
.status-label {
color: var(--muted);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.field-input {
width: 100%;
padding: 14px 16px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--text);
background: rgba(3, 8, 18, 0.72);
outline: none;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.field-input:focus {
border-color: rgba(112, 214, 255, 0.55);
box-shadow: 0 0 0 4px rgba(112, 214, 255, 0.12);
}
.primary-button,
.ghost-button {
border: 0;
border-radius: 16px;
padding: 14px 18px;
cursor: pointer;
transition: transform 0.18s ease, opacity 0.18s ease, background 0.18s ease;
}
.primary-button {
background: linear-gradient(135deg, var(--accent-strong), #7f8cff);
color: #04111b;
font-weight: 700;
min-width: 132px;
}
.ghost-button {
color: var(--text);
background: rgba(255, 255, 255, 0.06);
}
.primary-button:hover,
.ghost-button:hover {
transform: translateY(-1px);
}
.primary-button:disabled,
.ghost-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.toolbar-actions .ghost-button {
min-width: 132px;
}
.status-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
margin-top: 18px;
align-items: center;
}
.status-copy {
color: var(--muted);
line-height: 1.6;
}
.progress-track {
margin-top: 18px;
height: 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--accent), var(--accent-warm));
width: 0%;
transition: width 0.2s ease;
}
.gallery-header {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin: 20px 0 14px;
}
.gallery-title {
margin: 0;
font-family: var(--font-display), var(--font-body), sans-serif;
font-size: 18px;
}
.gallery-meta {
color: var(--muted);
font-family: var(--font-mono), monospace;
font-size: 13px;
}
.player-card {
margin-top: 18px;
padding: 18px;
border-radius: 22px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.scrubber {
margin-top: 16px;
padding: 14px 16px;
border-radius: 18px;
background: rgba(2, 5, 11, 0.42);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.scrubber-header {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin-bottom: 10px;
}
.scrubber-label {
color: var(--muted);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.scrubber-value {
color: var(--text);
font-family: var(--font-mono), monospace;
font-size: 13px;
}
.scrubber-input {
width: 100%;
height: 28px;
margin: 0;
background: transparent;
cursor: pointer;
accent-color: var(--accent-strong);
}
.scrubber-input:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.scrubber-input::-webkit-slider-runnable-track {
height: 8px;
border-radius: 999px;
background: linear-gradient(90deg, rgba(112, 214, 255, 0.9), rgba(244, 184, 96, 0.85));
border: 1px solid rgba(255, 255, 255, 0.08);
}
.scrubber-input::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
margin-top: -7px;
border-radius: 999px;
border: 2px solid rgba(5, 9, 18, 0.92);
background: #eef4ff;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
}
.scrubber-input::-moz-range-track {
height: 8px;
border-radius: 999px;
background: linear-gradient(90deg, rgba(112, 214, 255, 0.9), rgba(244, 184, 96, 0.85));
border: 1px solid rgba(255, 255, 255, 0.08);
}
.scrubber-input::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 999px;
border: 2px solid rgba(5, 9, 18, 0.92);
background: #eef4ff;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
}
.player-surface {
min-height: 240px;
}
.playback-canvas {
display: block;
width: 100%;
max-height: 72vh;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: #02050b;
}
.player-placeholder {
min-height: 240px;
display: grid;
place-items: center;
padding: 24px;
border-radius: 18px;
border: 1px dashed rgba(255, 255, 255, 0.14);
color: var(--muted);
text-align: center;
background: rgba(2, 5, 11, 0.72);
}
.pager {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.pager button {
appearance: none;
min-width: 44px;
padding: 10px 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: var(--text);
background: rgba(255, 255, 255, 0.05);
cursor: pointer;
transition: transform 0.18s ease, background 0.18s ease, border-color 0.18s ease;
}
.pager button.active {
border-color: rgba(112, 214, 255, 0.5);
background: rgba(112, 214, 255, 0.14);
}
.pager button:hover:not(:disabled) {
transform: translateY(-1px);
border-color: rgba(112, 214, 255, 0.35);
background: rgba(255, 255, 255, 0.08);
}
.empty-state,
.error-state,
.loading-state {
padding: 28px 18px;
border-radius: 18px;
border: 1px dashed rgba(255, 255, 255, 0.14);
color: var(--muted);
text-align: center;
}
.error-state {
color: #ffd2cf;
border-color: rgba(255, 134, 120, 0.3);
background: rgba(255, 87, 64, 0.08);
}
@media (max-width: 920px) {
.viewer-shell {
padding: 18px;
}
.hero-card,
.toolbar,
.status-row {
grid-template-columns: 1fr;
}
}

42
web/app/layout.tsx Normal file
View File

@@ -0,0 +1,42 @@
import type { Metadata } from "next";
import { IBM_Plex_Mono, Noto_Sans_SC, Space_Grotesk } from "next/font/google";
import "./globals.css";
const display = Space_Grotesk({
subsets: ["latin"],
variable: "--font-display",
display: "swap",
});
const body = Noto_Sans_SC({
subsets: ["latin"],
weight: ["400", "500", "700"],
variable: "--font-body",
display: "swap",
});
const mono = IBM_Plex_Mono({
subsets: ["latin"],
weight: ["400", "500"],
variable: "--font-mono",
display: "swap",
});
export const metadata: Metadata = {
title: "H.264 Frame Viewer",
description: "Rust backend + Next.js frontend for H.264 frame extraction",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN">
<body className={`${display.variable} ${body.variable} ${mono.variable}`}>
{children}
</body>
</html>
);
}

5
web/app/page.tsx Normal file
View File

@@ -0,0 +1,5 @@
import VideoFrameViewer from "@/components/VideoFrameViewer";
export default function Page() {
return <VideoFrameViewer />;
}