diff --git a/package.json b/package.json index 6811d7e..8961342 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "start": "next start", "test": "pnpm build && playwright test", "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui" + "test:e2e:ui": "playwright test --ui", + "optimize:banner": "node scripts/optimize-hero-banner.mjs" }, "dependencies": { "@headlessui/react": "2.1.2", @@ -31,6 +32,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "babel-plugin-react-compiler": "1.0.0", + "ffmpeg-static": "^5.3.0", "pagefind": "^1.5.2", "tailwindcss": "^4", "typescript": "^5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfbb445..842dd55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 + ffmpeg-static: + specifier: ^5.3.0 + version: 5.3.0 pagefind: specifier: ^1.5.2 version: 1.5.2 @@ -94,6 +97,10 @@ packages: '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + '@derhuerst/http-basic@8.2.4': + resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==} + engines: {node: '>=6.0.0'} + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} @@ -1017,6 +1024,9 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + '@types/node@10.17.60': + resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + '@types/node@20.19.41': resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} @@ -1062,6 +1072,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -1104,9 +1118,15 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1161,6 +1181,10 @@ packages: compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -1370,6 +1394,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + es-toolkit@1.46.1: resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} @@ -1437,6 +1465,10 @@ packages: picomatch: optional: true + ffmpeg-static@5.3.0: + resolution: {integrity: sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==} + engines: {node: '>=16'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1529,6 +1561,13 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-response-object@3.0.2: + resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -1540,6 +1579,9 @@ packages: import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -2015,6 +2057,9 @@ packages: resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==} hasBin: true + parse-cache-control@1.0.1: + resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -2076,6 +2121,10 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -2113,6 +2162,10 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + reading-time@1.5.0: resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} @@ -2213,6 +2266,9 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2268,6 +2324,9 @@ packages: resolution: {integrity: sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==} hasBin: true + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -2351,6 +2410,9 @@ packages: peerDependencies: typescript: ^5.5.0 || ^6.0.0 + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2400,6 +2462,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@14.0.0: resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true @@ -2475,6 +2540,13 @@ snapshots: '@chevrotain/types@11.1.2': {} + '@derhuerst/http-basic@8.2.4': + dependencies: + caseless: 0.12.0 + concat-stream: 2.0.0 + http-response-object: 3.0.2 + parse-cache-control: 1.0.1 + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 @@ -3336,6 +3408,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/node@10.17.60': {} + '@types/node@20.19.41': dependencies: undici-types: 6.21.0 @@ -3377,6 +3451,12 @@ snapshots: acorn@8.16.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + arg@5.0.2: {} aria-hidden@1.2.6: @@ -3410,8 +3490,12 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-from@1.1.2: {} + caniuse-lite@1.0.30001793: {} + caseless@0.12.0: {} + ccount@2.0.1: {} chalk@5.6.2: {} @@ -3448,6 +3532,13 @@ snapshots: compute-scroll-into-view@3.1.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -3681,6 +3772,8 @@ snapshots: entities@6.0.1: {} + env-paths@2.2.1: {} + es-toolkit@1.46.1: {} esast-util-from-estree@2.0.0: @@ -3772,6 +3865,15 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + ffmpeg-static@5.3.0: + dependencies: + '@derhuerst/http-basic': 8.2.4 + env-paths: 2.2.1 + https-proxy-agent: 5.0.1 + progress: 2.0.3 + transitivePeerDependencies: + - supports-color + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -3949,6 +4051,17 @@ snapshots: html-void-elements@3.0.0: {} + http-response-object@3.0.2: + dependencies: + '@types/node': 10.17.60 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@5.0.0: {} iconv-lite@0.6.3: @@ -3957,6 +4070,8 @@ snapshots: import-meta-resolve@4.2.0: {} + inherits@2.0.4: {} + inline-style-parser@0.2.7: {} internmap@1.0.1: {} @@ -4745,6 +4860,8 @@ snapshots: '@pagefind/windows-arm64': 1.5.2 '@pagefind/windows-x64': 1.5.2 + parse-cache-control@1.0.1: {} + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -4811,6 +4928,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + progress@2.0.3: {} + property-information@7.1.0: {} queue-microtask@1.2.3: {} @@ -4855,6 +4974,12 @@ snapshots: react@19.2.4: {} + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + reading-time@1.5.0: {} recma-build-jsx@1.0.0: @@ -5051,6 +5176,8 @@ snapshots: rw@1.3.3: {} + safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} scheduler@0.27.0: {} @@ -5129,6 +5256,10 @@ snapshots: commander: 13.1.0 wicked-good-xpath: 1.3.0 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -5199,6 +5330,8 @@ snapshots: transitivePeerDependencies: - supports-color + typedarray@0.0.6: {} + typescript@5.9.3: {} undici-types@6.21.0: {} @@ -5269,6 +5402,8 @@ snapshots: dependencies: react: 19.2.4 + util-deprecate@1.0.2: {} + uuid@14.0.0: {} vfile-location@5.0.3: diff --git a/public/banner.mp4 b/public/banner.mp4 new file mode 100644 index 0000000..5164e27 Binary files /dev/null and b/public/banner.mp4 differ diff --git a/public/banner.webm b/public/banner.webm new file mode 100644 index 0000000..0a7f426 Binary files /dev/null and b/public/banner.webm differ diff --git a/scripts/optimize-hero-banner.mjs b/scripts/optimize-hero-banner.mjs new file mode 100644 index 0000000..6bbe423 --- /dev/null +++ b/scripts/optimize-hero-banner.mjs @@ -0,0 +1,74 @@ +/** + * 将 public/banner.mp4(或 banner.source.mp4)压成首屏 Hero 尺寸。 + * 用法:pnpm run optimize:banner + * 可选:把未压缩母带重命名为 banner.source.mp4,脚本会优先读取它并写回 banner.mp4。 + */ +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import ffmpegPath from "ffmpeg-static"; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); +const publicDir = path.join(root, "public"); +const sourceCandidates = [ + path.join(publicDir, "banner.source.mp4"), + path.join(publicDir, "banner.mp4"), +]; +const input = sourceCandidates.find((p) => fs.existsSync(p)); + +if (!input || !ffmpegPath) { + console.error("缺少 banner.mp4 / banner.source.mp4 或 ffmpeg 二进制。"); + process.exit(1); +} + +const outMp4 = path.join(publicDir, "banner.mp4"); +const outWebm = path.join(publicDir, "banner.webm"); +const tmpMp4 = path.join(publicDir, "banner.opt.mp4"); + +const scale = "scale=1920:-2"; +const common = ["-y", "-i", input, "-an", "-vf", scale, "-pix_fmt", "yuv420p"]; + +console.log(`输入: ${input}`); + +execFileSync( + ffmpegPath, + [ + ...common, + "-c:v", + "libx264", + "-preset", + "slow", + "-crf", + "28", + "-movflags", + "+faststart", + tmpMp4, + ], + { stdio: "inherit" }, +); + +execFileSync( + ffmpegPath, + [ + ...common, + "-c:v", + "libvpx-vp9", + "-crf", + "35", + "-b:v", + "0", + "-row-mt", + "1", + outWebm, + ], + { stdio: "inherit" }, +); + +fs.copyFileSync(tmpMp4, outMp4); +fs.unlinkSync(tmpMp4); + +for (const f of [outMp4, outWebm]) { + const kb = (fs.statSync(f).size / 1024).toFixed(1); + console.log(`${path.basename(f)}: ${kb} KiB`); +} diff --git a/src/components/hero-banner-video.tsx b/src/components/hero-banner-video.tsx new file mode 100644 index 0000000..10e8ac0 --- /dev/null +++ b/src/components/hero-banner-video.tsx @@ -0,0 +1,163 @@ +"use client"; + +import Image from "next/image"; +import { useReducedMotion } from "motion/react"; +import { useEffect, useRef } from "react"; + +const POSTER = "/banner.png"; +const WEBM = "/banner.webm"; +const MP4 = "/banner.mp4"; + +const videoClassName = + "pointer-events-none absolute inset-0 h-full w-full object-cover object-center"; + +function HeroBannerSources() { + return ( + <> + + + + ); +} + +function seekToStart(video: HTMLVideoElement) { + if (typeof video.fastSeek === "function") { + try { + video.fastSeek(0); + return; + } catch { + // fall through + } + } + video.currentTime = 0; +} + +function primeVideo(el: HTMLVideoElement, preload: "auto" | "none") { + el.muted = true; + el.playsInline = true; + el.preload = preload; +} + +/** + * 双轨在 ended 时刻精确切换:不提前截断,保证末帧与首帧对齐。 + * 副轨 preload=none,仅在接力前加载,减少首屏带宽与解码占用。 + */ +export function HeroBannerVideo() { + const reduceMotion = useReducedMotion(); + const refA = useRef(null); + const refB = useRef(null); + + useEffect(() => { + if (reduceMotion) return; + + const videoA = refA.current; + const videoB = refB.current; + if (!videoA || !videoB) return; + + primeVideo(videoA, "auto"); + primeVideo(videoB, "none"); + + let active: HTMLVideoElement = videoA; + let idle: HTMLVideoElement = videoB; + let idlePrimed = false; + + const show = (el: HTMLVideoElement, visible: boolean) => { + el.style.opacity = visible ? "1" : "0"; + }; + + show(videoA, true); + show(videoB, false); + + const primeIdle = () => { + if (idlePrimed) return; + idlePrimed = true; + idle.preload = "auto"; + idle.load(); + }; + + const onEnded = (endedVideo: HTMLVideoElement) => { + if (endedVideo !== active) return; + + seekToStart(endedVideo); + endedVideo.pause(); + show(endedVideo, false); + + active = idle; + idle = endedVideo; + + primeIdle(); + seekToStart(active); + show(active, true); + void active.play().catch(() => {}); + }; + + const handleEndedA = () => onEnded(videoA); + const handleEndedB = () => onEnded(videoB); + + const warmIdleBeforeHandoff = (ev: Event) => { + const current = ev.currentTarget as HTMLVideoElement; + if (current !== active) return; + const duration = current.duration; + if (!Number.isFinite(duration) || duration <= 0) return; + if (duration - current.currentTime <= 1.5) { + primeIdle(); + } + }; + + videoA.addEventListener("ended", handleEndedA); + videoB.addEventListener("ended", handleEndedB); + videoA.addEventListener("timeupdate", warmIdleBeforeHandoff); + videoB.addEventListener("timeupdate", warmIdleBeforeHandoff); + + void videoA.play().catch(() => {}); + + return () => { + videoA.removeEventListener("ended", handleEndedA); + videoB.removeEventListener("ended", handleEndedB); + videoA.removeEventListener("timeupdate", warmIdleBeforeHandoff); + videoB.removeEventListener("timeupdate", warmIdleBeforeHandoff); + }; + }, [reduceMotion]); + + if (reduceMotion === true) { + return ( + + ); + } + + return ( + <> + + + + ); +} diff --git a/src/components/hero-section.tsx b/src/components/hero-section.tsx index e46c686..7f7ccbf 100644 --- a/src/components/hero-section.tsx +++ b/src/components/hero-section.tsx @@ -1,4 +1,4 @@ -import Image from "next/image"; +import { HeroBannerVideo } from "@/components/hero-banner-video"; import { site } from "@/content/site"; export function HeroSection() { @@ -8,14 +8,7 @@ export function HeroSection() { className="hero-section relative isolate z-0 overflow-hidden" id="top" > - 海宇数科品牌视觉 +