feat: Update Next.js to 16.1.4, bump project version to 4.0.0, refine DesktopMoreMenu UI, and remove AD_FILTER_GUIDE.md.

This commit is contained in:
kuekhaoyang
2026-01-21 19:13:21 +08:00
parent aec824ebcd
commit 49c37b372f
4 changed files with 60 additions and 178 deletions
-118
View File
@@ -1,118 +0,0 @@
# 🛡️ 广告过滤功能使用指南
KVideo 新增了强大的 M3U8 广告过滤系统,支持多种过滤模式,可有效去除视频中插入的切片广告。
## ✨ 功能特性
- **多模式选择**:支持关闭、关键词过滤、智能启发式过滤(Beta)和激进模式。
- **UI 集成**:在播放器设置菜单中直接切换模式,实时生效。
- **自定义关键词**:支持通过环境变量扩展过滤关键词。
- **高性能**:基于流式处理,对播放加载速度几乎无影响。
## 🎮 使用说明
1. 打开任意视频播放。
2. 点击播放器左上角的 **(···)** 按钮。
3.**广告过滤** 下拉菜单中选择模式:
- **关闭**:不过滤任何内容(默认)。
- **关键词**:仅过滤 URL 中包含特定广告关键词(如 `adjump`, `pre-roll`)的片段。安全稳定,误杀率极低。
- **智能(Beta)**:推荐使用。结合关键词、文件名特征、时间戳跳变标记 (DISCONTINUITY) 和 HLS 标签进行综合评分。能识别大多数无明显关键词的广告。
- **激进**:降低判定阈值,过滤更多疑似广告,但可能会误伤正片。仅在智能模式无效时尝试。
> ⚠️重要提示:添加关键词时,请勿使用类似`ad` `ads` `video` `20260118` 这类简短相对通用的词汇,会造成误杀率极高。
---
## 🛠️ 部署配置指南
你可以通过环境变量 `NEXT_PUBLIC_AD_KEYWORDS` 自定义额外的广告关键词。
### 1. 本地开发 (Local Development)
在项目根目录的 `.env.local` 文件中添加。支持多行格式(推荐使用引号包裹):
```env
# 单行模式
NEXT_PUBLIC_AD_KEYWORDS=spam,promo,intro-ad
# 多行模式 (更清晰)
NEXT_PUBLIC_AD_KEYWORDS="
spam
promo
intro-ad
/unwanted-segment/
"
```
### 2. Docker 部署 (运行时热加载)
我们支持三种方式注入关键词,优先级从高到低:
1. **文件加载 (推荐)**:通过挂载文件并在运行时读取。
2. **运行时环境变量**:直接通过 `-e` 传入 `AD_KEYWORDS`
3. **构建时环境变量**:构建镜像时打入的 `NEXT_PUBLIC_AD_KEYWORDS`
#### 方案 A:挂载配置文件 (支持运行时修改)
创建一个关键词文件 `ad_keywords.txt`
```text
spam
promo
/ad-segment/
intro
```
运行 Docker 时挂载该文件并设置 `AD_KEYWORDS_FILE` 环境变量:
```bash
docker run -d -p 3000:3000 \
-v $(pwd)/ad_keywords.txt:/app/ad_keywords.txt \
-e AD_KEYWORDS_FILE=/app/ad_keywords.txt \
--name kvideo kuekhaoyang/kvideo:latest
```
*注:修改宿主机文件后重启容器即可生效,无需重新构建镜像。*
#### 方案 B:直接注入环境变量
```bash
docker run -d -p 3000:3000 \
-e AD_KEYWORDS="spam,promo,intro-ad" \
--name kvideo kuekhaoyang/kvideo:latest
```
### 3. Vercel 部署
1. 进入 Vercel 项目 Dashboard。
2. 点击 **Settings** -> **Environment Variables**
3. 添加新变量:
- **Key**: `NEXT_PUBLIC_AD_KEYWORDS`
- **Value**: `spam,promo,intro-ad`
4. 重新部署项目以生效。
### 4. Cloudflare Pages 部署
1. 进入 Cloudflare Pages 项目 Dashboard。
2. 点击 **Settings** -> **Environment variables**
3. 添加变量:
- **Variable name**: `NEXT_PUBLIC_AD_KEYWORDS`
- **Value**: `spam,promo,intro-ad`
4. 重新部署项目 (Retry deployment) 以生效。
---
## 🔍 技术实现细节
广告检测逻辑位于 `lib/utils/m3u8-ad-detector.ts`,采用分块评分机制:
1. **Block 解析**:将 M3U8 播放列表按 `#EXT-X-DISCONTINUITY` 分割成多个块。
2. **特征学习**:分析最长的块(通常是正片)提取特征,如文件命名模式、路径前缀等。
3. **评分系统**
- **HLS 标签**`#EXT-X-CUE-OUT`/`IN` (+10分,确认为广告)
- **路径前缀**:与正片路径不一致 (+5分,高度疑似)
- **关键词**URL 包含已知广告词 (+2.5分)
- **文件名模式**:与正片命名规则不符 (+1.5分)
4. **决策过滤**
- **智能模式**:移除评分 >= 5.0 的块。
- **激进模式**:移除评分 >= 3.0 的块。
@@ -83,7 +83,7 @@ export function DesktopMoreMenu({
const MenuContent = (
<div
className="absolute z-[9999] bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] rounded-[var(--radius-2xl)] border border-[var(--glass-border)] shadow-[var(--shadow-md)] p-2 w-fit min-w-[220px] animate-in fade-in zoom-in-95 duration-200"
className="absolute z-[9999] bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] rounded-[var(--radius-2xl)] border border-[var(--glass-border)] shadow-[var(--shadow-md)] p-2 w-fit min-w-[240px] animate-in fade-in zoom-in-95 duration-200"
style={{
top: menuPosition.top,
left: menuPosition.left,
@@ -124,7 +124,7 @@ export function DesktopMoreMenu({
<div className="h-px bg-[var(--glass-border)] my-2" />
{/* Show Mode Indicator Switch */}
<div className="px-4 py-2.5 flex items-center justify-between">
<div className="px-4 py-2.5 flex items-center justify-between gap-6">
<div className="flex items-center gap-3 text-sm text-[var(--text-color)]">
<Icons.Zap size={18} />
<span></span>
@@ -144,7 +144,7 @@ export function DesktopMoreMenu({
</div>
{/* Ad Filter Mode Selector */}
<div className="px-4 py-2.5 flex items-center justify-between">
<div className="px-4 py-2.5 flex items-center justify-between gap-6">
<div className="flex items-center gap-3 text-sm text-[var(--text-color)]">
<Icons.ShieldAlert size={18} />
<span>广</span>
@@ -153,16 +153,16 @@ export function DesktopMoreMenu({
<div className="relative">
<button
onClick={() => setAdFilterOpen(!isAdFilterOpen)}
className="flex items-center gap-1.5 bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] text-xs rounded-md px-2.5 py-1.5 outline-none hover:border-[var(--accent-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_5%,transparent)] transition-all cursor-pointer"
className="flex items-center gap-1.5 bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] text-xs rounded-[var(--radius-2xl)] px-2.5 py-1.5 outline-none hover:border-[var(--accent-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_5%,transparent)] transition-all cursor-pointer"
>
<span>{AD_FILTER_LABELS[adFilterMode] || '关闭'}</span>
<Icons.ChevronDown size={14} className={`text-[var(--text-color-secondary)] transition-transform duration-200 ${isAdFilterOpen ? 'rotate-180' : ''}`} />
<Icons.ChevronDown size={14} className={`text-[var(--text-color-secondary)] transition-transform duration-300 ${isAdFilterOpen ? 'rotate-180' : ''}`} />
</button>
{isAdFilterOpen && (
<>
<div className="fixed inset-0 z-10 cursor-default" onClick={() => setAdFilterOpen(false)} />
<div className="absolute right-0 top-full mt-1 w-28 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] rounded-lg shadow-xl overflow-hidden z-20 flex flex-col animate-in fade-in zoom-in-95 duration-150">
<div className="absolute right-0 top-full mt-2 w-32 bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-md)] p-1.5 overflow-hidden z-20 flex flex-col animate-in fade-in zoom-in-95 duration-200">
{Object.entries(AD_FILTER_LABELS).map(([mode, label]) => (
<button
key={mode}
@@ -170,7 +170,7 @@ export function DesktopMoreMenu({
setAdFilterMode(mode as AdFilterMode);
setAdFilterOpen(false);
}}
className={`text-left text-xs px-3 py-2.5 hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)] transition-colors w-full flex items-center justify-between group ${adFilterMode === mode ? 'text-[var(--accent-color)] font-medium bg-[color-mix(in_srgb,var(--accent-color)_5%,transparent)]' : 'text-[var(--text-color)]'
className={`text-left text-xs px-3 py-2 rounded-[var(--radius-2xl)] hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)] transition-colors w-full flex items-center justify-between group ${adFilterMode === mode ? 'text-[var(--accent-color)] font-medium bg-[color-mix(in_srgb,var(--accent-color)_5%,transparent)]' : 'text-[var(--text-color)]'
}`}
>
<span>{label}</span>
+50 -50
View File
@@ -1,12 +1,12 @@
{
"name": "kvideo",
"version": "3.9.5",
"version": "4.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kvideo",
"version": "3.9.5",
"version": "4.0.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -14,7 +14,7 @@
"@vercel/analytics": "^1.6.1",
"hls.js": "^1.6.15",
"lucide-react": "^0.562.0",
"next": "16.1.2",
"next": "16.1.4",
"react": "19.2.3",
"react-dom": "19.2.3",
"zustand": "^5.0.10"
@@ -26,7 +26,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.2",
"eslint-config-next": "16.1.4",
"postcss": "^8.5.6",
"tailwindcss": "^4",
"typescript": "^5"
@@ -1306,15 +1306,15 @@
}
},
"node_modules/@next/env": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.2.tgz",
"integrity": "sha512-r6TpLovDTvWtzw11UubUQxEK6IduT8rSAHbGX68yeFpA/1Oq9R4ovi5nqMUMgPN0jr2SpfeyFRbTZg3Inuuv3g==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.4.tgz",
"integrity": "sha512-gkrXnZyxPUy0Gg6SrPQPccbNVLSP3vmW8LU5dwEttEEC1RwDivk8w4O+sZIjFvPrSICXyhQDCG+y3VmjlJf+9A==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.2.tgz",
"integrity": "sha512-jjO5BKDxZEXt2VCAnAG/ldULnpxeXspjCo9AZErV3Lm5HmNj8r2rS+eUMIAAj6mXPAOiPqAMgVPGnkyhPyDx4g==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.4.tgz",
"integrity": "sha512-38WMjGP8y+1MN4bcZFs+GTcBe0iem5GGTzFE5GWW/dWdRKde7LOXH3lQT2QuoquVWyfl2S0fQRchGmeacGZ4Wg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1322,9 +1322,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.2.tgz",
"integrity": "sha512-0N2baysDpTXASTVxTV+DkBnD97bo9PatUj8sHlKA+oR9CyvReaPQchQyhCbH0Jm0mC/Oka5F52intN+lNOhSlA==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.4.tgz",
"integrity": "sha512-T8atLKuvk13XQUdVLCv1ZzMPgLPW0+DWWbHSQXs0/3TjPrKNxTmUIhOEaoEyl3Z82k8h/gEtqyuoZGv6+Ugawg==",
"cpu": [
"arm64"
],
@@ -1338,9 +1338,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.2.tgz",
"integrity": "sha512-Q0wnSK0lmeC9ps+/w/bDsMSF3iWS45WEwF1bg8dvMH3CmKB2BV4346tVrjWxAkrZq20Ro6Of3R19IgrEJkXKyw==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.4.tgz",
"integrity": "sha512-AKC/qVjUGUQDSPI6gESTx0xOnOPQ5gttogNS3o6bA83yiaSZJek0Am5yXy82F1KcZCx3DdOwdGPZpQCluonuxg==",
"cpu": [
"x64"
],
@@ -1354,9 +1354,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.2.tgz",
"integrity": "sha512-4twW+h7ZatGKWq+2pUQ9SDiin6kfZE/mY+D8jOhSZ0NDzKhQfAPReXqwTDWVrNjvLzHzOcDL5kYjADHfXL/b/Q==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.4.tgz",
"integrity": "sha512-POQ65+pnYOkZNdngWfMEt7r53bzWiKkVNbjpmCt1Zb3V6lxJNXSsjwRuTQ8P/kguxDC8LRkqaL3vvsFrce4dMQ==",
"cpu": [
"arm64"
],
@@ -1370,9 +1370,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.2.tgz",
"integrity": "sha512-Sn6LxPIZcADe5AnqqMCfwBv6vRtDikhtrjwhu+19WM6jHZe31JDRcGuPZAlJrDk6aEbNBPUUAKmySJELkBOesg==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.4.tgz",
"integrity": "sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q==",
"cpu": [
"arm64"
],
@@ -1386,9 +1386,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.2.tgz",
"integrity": "sha512-nwzesEQBfQIOOnQ7JArzB08w9qwvBQ7nC1i8gb0tiEFH94apzQM3IRpY19MlE8RBHxc9ArG26t1DEg2aaLaqVQ==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.4.tgz",
"integrity": "sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ==",
"cpu": [
"x64"
],
@@ -1402,9 +1402,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.2.tgz",
"integrity": "sha512-s60bLf16BDoICQHeKEm0lDgUNMsL1UpQCkRNZk08ZNnRpK0QUV+6TvVHuBcIA7oItzU0m7kVmXe8QjXngYxJVA==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.4.tgz",
"integrity": "sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw==",
"cpu": [
"x64"
],
@@ -1418,9 +1418,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.2.tgz",
"integrity": "sha512-Sq8k4SZd8Y8EokKdz304TvMO9HoiwGzo0CTacaiN1bBtbJSQ1BIwKzNFeFdxOe93SHn1YGnKXG6Mq3N+tVooyQ==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.4.tgz",
"integrity": "sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw==",
"cpu": [
"arm64"
],
@@ -1434,9 +1434,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.2.tgz",
"integrity": "sha512-KQDBwspSaNX5/wwt6p7ed5oINJWIxcgpuqJdDNubAyq7dD+ZM76NuEjg8yUxNOl5R4NNgbMfqE/RyNrsbYmOKg==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.4.tgz",
"integrity": "sha512-JSVlm9MDhmTXw/sO2PE/MRj+G6XOSMZB+BcZ0a7d6KwVFZVpkHcb2okyoYFBaco6LeiL53BBklRlOrDDbOeE5w==",
"cpu": [
"x64"
],
@@ -3863,13 +3863,13 @@
}
},
"node_modules/eslint-config-next": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.2.tgz",
"integrity": "sha512-y97rpFfUsaXdXlQc2FMl/yqRc5yfVVKtKRcv+7LeyBrKh83INFegJuZBE28dc9Chp4iKXwmjaW4sHHx/mgyDyA==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.4.tgz",
"integrity": "sha512-iCrrNolUPpn/ythx0HcyNRfUBgTkaNBXByisKUbusPGCl8DMkDXXAu7exlSTSLGTIsH9lFE/c4s/3Qiyv2qwdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/eslint-plugin-next": "16.1.2",
"@next/eslint-plugin-next": "16.1.4",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.32.0",
@@ -5906,12 +5906,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.1.2",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.2.tgz",
"integrity": "sha512-SVSWX7wjUUDrIDVqhl4xm/jiOrvYGMG7NzVE/dGzzgs7r3dFGm4V19ia0xn3GDNtHCKM7C9h+5BoimnJBhmt9A==",
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.4.tgz",
"integrity": "sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ==",
"license": "MIT",
"dependencies": {
"@next/env": "16.1.2",
"@next/env": "16.1.4",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579",
@@ -5925,14 +5925,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.1.2",
"@next/swc-darwin-x64": "16.1.2",
"@next/swc-linux-arm64-gnu": "16.1.2",
"@next/swc-linux-arm64-musl": "16.1.2",
"@next/swc-linux-x64-gnu": "16.1.2",
"@next/swc-linux-x64-musl": "16.1.2",
"@next/swc-win32-arm64-msvc": "16.1.2",
"@next/swc-win32-x64-msvc": "16.1.2",
"@next/swc-darwin-arm64": "16.1.4",
"@next/swc-darwin-x64": "16.1.4",
"@next/swc-linux-arm64-gnu": "16.1.4",
"@next/swc-linux-arm64-musl": "16.1.4",
"@next/swc-linux-x64-gnu": "16.1.4",
"@next/swc-linux-x64-musl": "16.1.4",
"@next/swc-win32-arm64-msvc": "16.1.4",
"@next/swc-win32-x64-msvc": "16.1.4",
"sharp": "^0.34.4"
},
"peerDependencies": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "kvideo",
"version": "3.9.5",
"version": "4.0.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -16,7 +16,7 @@
"@vercel/analytics": "^1.6.1",
"hls.js": "^1.6.15",
"lucide-react": "^0.562.0",
"next": "16.1.2",
"next": "16.1.4",
"react": "19.2.3",
"react-dom": "19.2.3",
"zustand": "^5.0.10"
@@ -28,7 +28,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.2",
"eslint-config-next": "16.1.4",
"postcss": "^8.5.6",
"tailwindcss": "^4",
"typescript": "^5"