feat: 修复超长 URL (414) 问题,添加分辨率检测,修复高级模式 bug,提升兼容性

- 将 groupedSources 存储在 sessionStorage 中而非 URL 参数里,以防止因 URL 过长导致的 CDN 414 错误 (#112)
- 自动将旧版 groupedSources URL 迁移至短 gs 键
- 从视频元素中检测并显示实际视频分辨率(例如 1920x1080),不再依赖来源提供的标签
- 修复 PlayerNavbar 设置链接在高级模式下总是跳转到 /settings 而非 /premium/settings 的问题 (#106)
- 修复 WatchHistorySidebar 未向 HistoryList 传递 isPremium,导致历史记录项 URL 丢失 premium=1 参数的问题 (#106)
- 为 HLS 播放添加多级浏览器回退机制 (HLS.js → 原生 → 代理 → 错误并提供浏览器建议) (#104)
- 在 IPTV 播放器中添加 HEVC/H.265 级别过滤,防止 CCTV 等频道出现仅有音频播放的问题 (#81)
- 更新依赖并升级版本至 4.5.0
This commit is contained in:
kuekhaoyang
2026-03-12 22:58:30 +08:00
parent dd760153e3
commit 0e87c94a64
14 changed files with 344 additions and 57 deletions
+17
View File
@@ -58,6 +58,7 @@
- **搜索结果显示**:支持默认显示和合并同名源两种模式
- **实时延迟监测**:可选实时显示各源的网络延迟
- **清晰度标签**:自动解析并显示视频清晰度(4K/蓝光/1080P/720P/HD 等),方便快速分辨源质量
- **实际分辨率检测**:播放视频时自动检测并显示实际视频分辨率(如 1920x1080),不依赖源标签,显示真实清晰度
- **繁体中文搜索**:自动将繁体中文转换为简体中文进行搜索,确保繁体输入也能搜到结果
- **源过滤**:支持按源和类型筛选搜索结果,源标签支持按类型分组显示,智能合并同名分类标签,展开/折叠状态持久化记忆
- **多级标签**:搜索结果和播放器中显示源名称和内容类型双重标签
@@ -70,11 +71,13 @@
- **延迟排序**:线路按网络延迟自动排序,最快的源排在前面
- **源切换**:在线路列表中快速切换到其他源,支持断点续播
- **自动切源**:当当前源不可用时,自动切换到延迟最低的可用源
- **短链接优化**:使用 sessionStorage 缓存源数据,避免 URL 过长导致 CDN 414 错误
### IPTV 直播
- **M3U 播放列表**:支持导入和管理 M3U/M3U8 格式的 IPTV 源
- **JSON 频道列表**:支持导入 JSON 格式的频道列表(数组或对象格式,自动识别)
- **HEVC 智能兼容**:自动检测 HEVC/H.265 编码流,优先选择 H.264 级别以避免音画不同步或仅有声音问题
- **频道网格**:按分组展示频道,支持分页浏览,大列表搜索优化
- **多级频道列表**:播放器内按源分组 → 按分类分组 → 频道的三级列表导航
- **多线路折叠**:频道多线路默认显示前 3 条,可点击展开查看全部
@@ -848,6 +851,20 @@ Android 7.0 (API 24) 的 WebView 基于 Chrome 51,不支持本项目使用的
KVideo 已内置代理服务器自动处理 CORS 问题和 HLS URL 重写,大部分 HLS 直播流应能正常播放。
### IPTV CCTV 等频道只有声音没有画面
部分 CCTV 和卫视频道使用 HEVC (H.265) 编码,某些浏览器不支持硬件解码 HEVC。KVideo v4.5.0+ 已自动检测 HEVC 流并优先选择 H.264 级别以提高兼容性。如果问题仍存在,建议使用 Chrome 或 Edge 浏览器。
### 部分浏览器无法播放视频
一些内置浏览器(如 vivo 浏览器、QQ 浏览器等)的 WebView 可能不完整支持 MSE (Media Source Extensions) 和 HLS.js。建议使用以下浏览器:
- Chrome(推荐)
- Edge
- SafariiOS/macOS
- Firefox
KVideo v4.5.0+ 已增加多级回退机制,会依次尝试 HLS.js、原生 HLS、代理播放等方式。
## 贡献代码
我们非常欢迎各种形式的贡献!无论是报告 Bug、提出新功能建议、改进文档,还是提交代码,你的每一份贡献都让这个项目变得更好。
+45 -9
View File
@@ -18,6 +18,7 @@ import { settingsStore } from '@/lib/store/settings-store';
import { premiumModeSettingsStore } from '@/lib/store/premium-mode-settings';
import { SegmentedControl } from '@/components/ui/SegmentedControl';
import { getSourceName } from '@/lib/utils/source-names';
import { retrieveGroupedSources, storeGroupedSources } from '@/lib/utils/grouped-sources-cache';
function PlayerContent() {
const searchParams = useSearchParams();
@@ -29,7 +30,9 @@ function PlayerContent() {
const source = searchParams.get('source');
const title = searchParams.get('title');
const episodeParam = searchParams.get('episode');
// Support both legacy 'groupedSources' (full JSON) and new 'gs' (sessionStorage key)
const groupedSourcesParam = searchParams.get('groupedSources');
const gsKey = searchParams.get('gs');
// Track settings - use mode-specific store
const modeStore = isPremium ? premiumModeSettingsStore : settingsStore;
@@ -45,6 +48,24 @@ function PlayerContent() {
setIsReversed(modeStore.getSettings().episodeReverseOrder);
}, []);
// Migrate legacy long groupedSources URL to short gs key
useEffect(() => {
if (groupedSourcesParam && !gsKey) {
try {
const data = JSON.parse(groupedSourcesParam);
if (Array.isArray(data) && data.length > 0) {
const newKey = storeGroupedSources(data);
if (newKey) {
const params = new URLSearchParams(searchParams.toString());
params.delete('groupedSources');
params.set('gs', newKey);
router.replace(`/player?${params.toString()}`, { scroll: false });
}
}
} catch { /* ignore parse errors */ }
}
}, []); // Run once on mount
// Redirect if no video ID or source
if (!videoId || !source) {
router.push('/');
@@ -74,7 +95,12 @@ function PlayerContent() {
const groupedSources = useMemo<SourceInfo[]>(() => {
let sources: SourceInfo[] = [];
if (groupedSourcesParam) {
// Try sessionStorage cache first (new short URL), then fall back to URL param (legacy)
if (gsKey) {
const cached = retrieveGroupedSources(gsKey);
if (cached) sources = cached;
} else if (groupedSourcesParam) {
try {
sources = JSON.parse(groupedSourcesParam);
} catch {
@@ -108,7 +134,7 @@ function PlayerContent() {
}
return sources;
}, [groupedSourcesParam, source, videoId, videoData?.vod_pic, discoveredSources]);
}, [gsKey, groupedSourcesParam, source, videoId, videoData?.vod_pic, discoveredSources]);
// Wire up the source unavailable handler now that groupedSources is defined
sourceUnavailableRef.current = () => {
@@ -131,7 +157,13 @@ function PlayerContent() {
params.set('source', best.source);
params.set('title', title || '');
if (episodeParam) params.set('episode', episodeParam);
if (groupedSourcesParam) params.set('groupedSources', groupedSourcesParam);
// Use short gs key for grouped sources
if (gsKey) {
params.set('gs', gsKey);
} else if (groupedSources.length > 1) {
const newKey = storeGroupedSources(groupedSources);
if (newKey) params.set('gs', newKey);
}
if (isPremium) params.set('premium', '1');
router.replace(`/player?${params.toString()}`, { scroll: false });
};
@@ -150,7 +182,10 @@ function PlayerContent() {
// Check if existing grouped sources already have full info (pic + latency)
let existingSources: SourceInfo[] = [];
if (groupedSourcesParam) {
if (gsKey) {
const cached = retrieveGroupedSources(gsKey);
if (cached) existingSources = cached;
} else if (groupedSourcesParam) {
try { existingSources = JSON.parse(groupedSourcesParam); } catch {}
}
// Always fetch alternatives if there's a pending fallback (source unavailable)
@@ -224,7 +259,7 @@ function PlayerContent() {
})();
return () => controller.abort();
}, [title, source, groupedSourcesParam, isPremium]);
}, [title, source, gsKey, groupedSourcesParam, isPremium]);
// Track current source for switching
const [currentSourceId, setCurrentSourceId] = useState(source);
@@ -401,12 +436,13 @@ function PlayerContent() {
if (playerTimeRef.current > 1) {
params.set('t', Math.floor(playerTimeRef.current).toString());
}
// Pass all known sources so switching persists
// Store all known sources using short gs key
const allSources = groupedSources.length > 0 ? groupedSources : [];
if (allSources.length > 1) {
params.set('groupedSources', JSON.stringify(allSources));
} else if (groupedSourcesParam) {
params.set('groupedSources', groupedSourcesParam);
const newKey = storeGroupedSources(allSources);
if (newKey) params.set('gs', newKey);
} else if (gsKey) {
params.set('gs', gsKey);
}
if (isPremium) {
params.set('premium', '1');
+6 -2
View File
@@ -9,6 +9,7 @@ import { formatTime, formatDate } from '@/lib/utils/format-utils';
import { PosterImage } from './PosterImage';
import { FavoriteButton } from '@/components/favorites/FavoriteButton';
import { getSourceName } from '@/lib/utils/source-names';
import { storeGroupedSources } from '@/lib/utils/grouped-sources-cache';
import type { VideoHistoryItem } from '@/lib/types';
interface HistoryItemProps {
@@ -25,14 +26,17 @@ export function HistoryItem({ item, onRemove, isPremium = false }: HistoryItemPr
title: item.title,
episode: item.episodeIndex.toString(),
});
// Pass sourceMap as groupedSources for source switching
// Store sourceMap in sessionStorage to avoid long URLs
if (item.sourceMap && Object.keys(item.sourceMap).length > 1) {
const groupData = Object.entries(item.sourceMap).map(([sourceName, videoId]) => ({
id: videoId,
source: sourceName,
sourceName: getSourceName(sourceName),
}));
params.set('groupedSources', JSON.stringify(groupData));
const cacheKey = storeGroupedSources(groupData);
if (cacheKey) {
params.set('gs', cacheKey);
}
}
if (isPremium) {
params.set('premium', '1');
@@ -114,6 +114,7 @@ export function WatchHistorySidebar({ isPremium = false }: { isPremium?: boolean
<HistoryList
history={viewingHistory}
onRemove={handleDeleteItem}
isPremium={isPremium}
/>
<HistoryFooter
+27 -2
View File
@@ -21,6 +21,8 @@ const HLS_LIVE_CONFIG: Partial<Hls['config']> = {
manifestLoadingMaxRetry: 3,
levelLoadingTimeOut: 10000,
fragLoadingTimeOut: 20000,
// Prefer H.264 (avc) over HEVC (hev/hvc) for maximum browser compatibility
preferManagedMediaSource: false,
};
const LOADING_TIMEOUT_MS = 30000;
@@ -275,7 +277,10 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
hlsRef.current = hlsProxy;
hlsProxy.loadSource(proxiedUrl);
hlsProxy.attachMedia(video);
hlsProxy.on(Hls.Events.MANIFEST_PARSED, () => {
// Filter HEVC levels for proxy attempt too
hlsProxy.on(Hls.Events.MANIFEST_PARSED, (_, data) => {
filterHEVCLevels(hlsProxy);
markLoaded();
video.play().catch(() => {});
});
@@ -292,10 +297,30 @@ export function IPTVPlayer({ channel, onClose, channels, onChannelChange, channe
});
};
// Helper: Filter out HEVC levels that browser may not support (fixes audio-only issue)
const filterHEVCLevels = (hlsInstance: Hls) => {
if (!hlsInstance.levels || hlsInstance.levels.length <= 1) return;
const h264Levels = hlsInstance.levels
.map((level, index) => ({ level, index }))
.filter(({ level }) => {
const codec = level.videoCodec?.toLowerCase() || '';
// Keep levels without HEVC codec (H.264 or unknown)
return !codec.includes('hev') && !codec.includes('h265') && !codec.includes('hvc');
});
// If we have H.264 levels, restrict to those
if (h264Levels.length > 0 && h264Levels.length < hlsInstance.levels.length) {
console.info('[IPTV] Filtering HEVC levels, using H.264 only for compatibility');
// Set level to first H.264 level
hlsInstance.currentLevel = h264Levels[0].index;
}
};
// First try direct URL with HLS.js
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
hls.on(Hls.Events.MANIFEST_PARSED, (_, data) => {
// Filter HEVC levels to prevent audio-only playback
filterHEVCLevels(hls);
markLoaded();
video.play().catch(() => {});
});
+14
View File
@@ -6,6 +6,7 @@ import { useDesktopPlayerLogic } from './hooks/useDesktopPlayerLogic';
import { useHlsPlayer } from './hooks/useHlsPlayer';
import { useAutoSkip } from './hooks/useAutoSkip';
import { useStallDetection } from './hooks/useStallDetection';
import { useVideoResolution } from './hooks/useVideoResolution';
import { DesktopControlsWrapper } from './desktop/DesktopControlsWrapper';
import { DesktopOverlayWrapper } from './desktop/DesktopOverlayWrapper';
import { DanmakuCanvas } from './DanmakuCanvas';
@@ -51,6 +52,9 @@ export function DesktopVideoPlayer({
const isIOS = useIsIOS();
const isMobile = useIsMobile();
// Detect actual video resolution
const videoResolution = useVideoResolution(refs.videoRef);
// Danmaku
const { danmakuEnabled, setDanmakuEnabled, comments: danmakuComments } = useDanmaku({
videoTitle,
@@ -231,6 +235,16 @@ export function DesktopVideoPlayer({
/>
)}
{/* Video Resolution Badge - shows actual resolution from video stream */}
{videoResolution && (
<div className="absolute top-3 left-3 z-20 pointer-events-none">
<span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold text-white ${videoResolution.color} opacity-80`}>
{videoResolution.label}
<span className="font-normal opacity-80">{videoResolution.width}x{videoResolution.height}</span>
</span>
</div>
)}
<DesktopOverlayWrapper
data={data}
actions={actions}
+1 -1
View File
@@ -38,7 +38,7 @@ export function PlayerNavbar({ isPremium }: { isPremium?: boolean }) {
</div>
<div className="flex items-center gap-3">
<Link
href="/settings"
href={isPremium ? '/premium/settings' : '/settings'}
className="w-10 h-10 flex items-center justify-center rounded-[var(--radius-full)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)] transition-all duration-200 cursor-pointer"
aria-label="设置"
>
+7
View File
@@ -40,6 +40,13 @@ export function VideoPlayerError({
<h3></h3>
<p>{error}</p>
{/* Browser compatibility hint */}
{(error.includes('不支持') || error.includes('格式') || error.includes('编码')) && (
<p className="text-xs text-white/50 mt-1">
使 ChromeEdge Safari
</p>
)}
{/* Action Buttons */}
<div className="flex gap-3 justify-center flex-wrap">
<button
+26 -3
View File
@@ -38,7 +38,10 @@ export function useHlsPlayer({
// Check if HLS is supported natively (Safari, Mobile Chrome)
const isNativeHlsSupported = video.canPlayType('application/vnd.apple.mpegurl');
if (Hls.isSupported()) {
// Check if MSE is available (required by HLS.js)
const isMSESupported = Hls.isSupported();
if (isMSESupported) {
// Define custom loader class to intercept manifest loading
// We use 'any' cast because default loader type might not be strictly exposed in all typings
@@ -367,8 +370,28 @@ export function useHlsPlayer({
video.src = src;
}
} else {
console.error('[HLS] HLS not supported');
onError?.('当前浏览器不支持 HLS 视频播放');
// Neither MSE nor native HLS supported
// Try direct playback as last resort (works for mp4 and some browser WebView)
console.warn('[HLS] No MSE or native HLS support. Trying direct playback...');
video.src = src;
let directFailed = false;
const handleCanPlay = () => {
directFailed = false;
};
const handleError = () => {
if (directFailed) return;
directFailed = true;
// Try proxied URL as final attempt
const proxiedUrl = `/api/proxy?url=${encodeURIComponent(src)}`;
video.src = proxiedUrl;
video.addEventListener('error', () => {
onError?.('当前浏览器不支持 HLS 视频播放。建议使用 Chrome、Edge 或 Safari 浏览器。');
}, { once: true });
};
video.addEventListener('canplay', handleCanPlay, { once: true });
video.addEventListener('error', handleError, { once: true });
}
return () => {
@@ -0,0 +1,66 @@
import { useState, useEffect } from 'react';
/**
* Maps video resolution height to a human-readable quality label.
*/
function getResolutionLabel(width: number, height: number): { label: string; color: string } | null {
if (width === 0 || height === 0) return null;
// Use the larger dimension in case of portrait video
const h = Math.max(width, height) === width ? height : width;
if (h >= 2160) return { label: '4K', color: 'bg-amber-500' };
if (h >= 1440) return { label: '2K', color: 'bg-emerald-500' };
if (h >= 1080) return { label: '1080P', color: 'bg-green-500' };
if (h >= 720) return { label: '720P', color: 'bg-teal-500' };
if (h >= 480) return { label: '480P', color: 'bg-sky-500' };
if (h >= 360) return { label: '360P', color: 'bg-gray-500' };
return { label: `${h}P`, color: 'bg-gray-500' };
}
export interface VideoResolutionInfo {
width: number;
height: number;
label: string;
color: string;
}
/**
* Detects the actual video resolution from the <video> element
* after metadata is loaded.
*/
export function useVideoResolution(videoRef: React.RefObject<HTMLVideoElement | null>): VideoResolutionInfo | null {
const [resolution, setResolution] = useState<VideoResolutionInfo | null>(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const detectResolution = () => {
const w = video.videoWidth;
const h = video.videoHeight;
if (w > 0 && h > 0) {
const info = getResolutionLabel(w, h);
if (info) {
setResolution({ width: w, height: h, ...info });
}
}
};
// Detect on loadedmetadata and also on resize (quality change)
video.addEventListener('loadedmetadata', detectResolution);
video.addEventListener('resize', detectResolution);
// Check if already loaded
if (video.videoWidth > 0 && video.videoHeight > 0) {
detectResolution();
}
return () => {
video.removeEventListener('loadedmetadata', detectResolution);
video.removeEventListener('resize', detectResolution);
};
}, [videoRef]);
return resolution;
}
+7 -3
View File
@@ -15,6 +15,7 @@ import { LatencyBadge } from '@/components/ui/LatencyBadge';
import { FavoriteButton } from '@/components/favorites/FavoriteButton';
import { Video } from '@/lib/types';
import { parseVideoTitle } from '@/lib/utils/video';
import { storeGroupedSources } from '@/lib/utils/grouped-sources-cache';
export interface GroupedVideo {
/** Representative video (lowest latency) */
@@ -50,7 +51,7 @@ export const VideoGroupCard = memo<VideoGroupCardProps>(({
return currentLatencies.length > 0 ? Math.min(...currentLatencies) : undefined;
}, [videos, latencies]);
// Generate URL with grouped sources data
// Generate URL with grouped sources stored in sessionStorage (avoids long URLs / 414 errors)
const videoUrl = useMemo(() => {
const params = new URLSearchParams({
id: String(representative.vod_id),
@@ -58,7 +59,7 @@ export const VideoGroupCard = memo<VideoGroupCardProps>(({
title: representative.vod_name,
});
// Add group data if multiple sources
// Store group data in sessionStorage and pass short key in URL
if (videos.length > 1) {
const groupData = videos.map(v => ({
id: v.vod_id,
@@ -69,7 +70,10 @@ export const VideoGroupCard = memo<VideoGroupCardProps>(({
typeName: v.type_name,
remarks: v.vod_remarks,
}));
params.set('groupedSources', JSON.stringify(groupData));
const cacheKey = storeGroupedSources(groupData);
if (cacheKey) {
params.set('gs', cacheKey);
}
}
return `/player?${params.toString()}`;
+85
View File
@@ -0,0 +1,85 @@
/**
* Grouped Sources Cache
* Stores groupedSources data in sessionStorage to avoid extremely long URLs
* that cause HTTP 414 errors on CDNs (e.g., Cloudflare, AWS CloudFront).
*
* Instead of passing the full JSON array in the URL parameter,
* we store it in sessionStorage and pass a short key in the URL.
*/
const CACHE_PREFIX = 'gs:';
const MAX_CACHE_SIZE = 100;
/**
* Store grouped sources data and return a short cache key.
*/
export function storeGroupedSources(data: any[]): string {
if (typeof window === 'undefined') return '';
const key = generateKey();
try {
// Cleanup old entries if too many
cleanupOldEntries();
sessionStorage.setItem(
`${CACHE_PREFIX}${key}`,
JSON.stringify({ data, ts: Date.now() })
);
} catch {
// sessionStorage full or unavailable — fall back gracefully
}
return key;
}
/**
* Retrieve grouped sources data by cache key.
*/
export function retrieveGroupedSources(key: string): any[] | null {
if (typeof window === 'undefined' || !key) return null;
try {
const raw = sessionStorage.getItem(`${CACHE_PREFIX}${key}`);
if (!raw) return null;
const parsed = JSON.parse(raw);
return parsed?.data || null;
} catch {
return null;
}
}
/**
* Generate a short random key (8 chars, base36).
*/
function generateKey(): string {
return Math.random().toString(36).slice(2, 10);
}
/**
* Remove oldest entries when cache exceeds max size.
*/
function cleanupOldEntries(): void {
try {
const entries: { key: string; ts: number }[] = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key?.startsWith(CACHE_PREFIX)) {
try {
const raw = sessionStorage.getItem(key);
const parsed = raw ? JSON.parse(raw) : null;
entries.push({ key, ts: parsed?.ts || 0 });
} catch {
entries.push({ key, ts: 0 });
}
}
}
if (entries.length >= MAX_CACHE_SIZE) {
// Sort by timestamp ascending and remove oldest half
entries.sort((a, b) => a.ts - b.ts);
const toRemove = entries.slice(0, Math.floor(entries.length / 2));
for (const entry of toRemove) {
sessionStorage.removeItem(entry.key);
}
}
} catch {
// Ignore cleanup errors
}
}
+36 -31
View File
@@ -1,20 +1,20 @@
{
"name": "kvideo",
"version": "4.4.9",
"version": "4.5.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "kvideo",
"version": "4.4.9",
"version": "4.5.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@upstash/redis": "^1.34.4",
"@vercel/analytics": "^1.6.1",
"@upstash/redis": "^1.37.0",
"@vercel/analytics": "^2.0.0",
"hls.js": "^1.6.15",
"lucide-react": "^0.575.0",
"lucide-react": "^0.577.0",
"next": "16.1.6",
"opencc-js": "^1.0.5",
"react": "19.2.4",
@@ -29,8 +29,8 @@
"@types/react-dom": "^19",
"eslint": "^10",
"eslint-config-next": "16.1.6",
"postcss": "^8.5.6",
"postcss-preset-env": "^11.1.3",
"postcss": "^8.5.8",
"postcss-preset-env": "^11.2.0",
"tailwindcss": "^4",
"typescript": "^5"
}
@@ -3498,21 +3498,24 @@
]
},
"node_modules/@upstash/redis": {
"version": "1.36.2",
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.36.2.tgz",
"integrity": "sha512-C0Yt8hc12vLaQYRG1fMci8iPrLtnTdbJG0HR5T8vKnvEP/1RdMMblsOJs5/jp0JXZJ1oSzMnQz4J9EVezNpI6A==",
"version": "1.37.0",
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.37.0.tgz",
"integrity": "sha512-LqOJ3+XWPLSZ2rGSed5DYG3ixybxb8EhZu3yQqF7MdZX1wLBG/FRcI6xcUZXHy/SS7mmXWyadrud0HJHkOc+uw==",
"license": "MIT",
"dependencies": {
"uncrypto": "^0.1.3"
}
},
"node_modules/@vercel/analytics": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz",
"integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-2.0.0.tgz",
"integrity": "sha512-fP/ASXXz+1K/C2vWTnocd8RsGnkO9f1qOIDrhgQ3DagJtnea1EsM9AV9fDzjXlPIPb2vBQapxOIMCjtGIW8PZw==",
"license": "MIT",
"peerDependencies": {
"@remix-run/react": "^2",
"@sveltejs/kit": "^1 || ^2",
"next": ">= 13",
"nuxt": ">= 3",
"react": "^18 || ^19 || ^19.0.0-rc",
"svelte": ">= 4",
"vue": "^3",
@@ -6783,9 +6786,10 @@
}
},
"node_modules/lucide-react": {
"version": "0.575.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz",
"integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==",
"version": "0.577.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz",
"integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
@@ -7326,9 +7330,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"dev": true,
"funding": [
{
@@ -7344,6 +7348,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11050,17 +11055,17 @@
"optional": true
},
"@upstash/redis": {
"version": "1.36.2",
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.36.2.tgz",
"integrity": "sha512-C0Yt8hc12vLaQYRG1fMci8iPrLtnTdbJG0HR5T8vKnvEP/1RdMMblsOJs5/jp0JXZJ1oSzMnQz4J9EVezNpI6A==",
"version": "1.37.0",
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.37.0.tgz",
"integrity": "sha512-LqOJ3+XWPLSZ2rGSed5DYG3ixybxb8EhZu3yQqF7MdZX1wLBG/FRcI6xcUZXHy/SS7mmXWyadrud0HJHkOc+uw==",
"requires": {
"uncrypto": "^0.1.3"
}
},
"@vercel/analytics": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz",
"integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg=="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-2.0.0.tgz",
"integrity": "sha512-fP/ASXXz+1K/C2vWTnocd8RsGnkO9f1qOIDrhgQ3DagJtnea1EsM9AV9fDzjXlPIPb2vBQapxOIMCjtGIW8PZw=="
},
"acorn": {
"version": "8.16.0",
@@ -13154,9 +13159,9 @@
}
},
"lucide-react": {
"version": "0.575.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz",
"integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg=="
"version": "0.577.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz",
"integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="
},
"magic-string": {
"version": "0.30.21",
@@ -13511,9 +13516,9 @@
"dev": true
},
"postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"dev": true,
"requires": {
"nanoid": "^3.3.11",
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "kvideo",
"version": "4.4.9",
"version": "4.5.0",
"private": true,
"scripts": {
"dev": "next dev --port ${PORT:-3000}",
@@ -13,10 +13,10 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@upstash/redis": "^1.34.4",
"@vercel/analytics": "^1.6.1",
"@upstash/redis": "^1.37.0",
"@vercel/analytics": "^2.0.0",
"hls.js": "^1.6.15",
"lucide-react": "^0.575.0",
"lucide-react": "^0.577.0",
"next": "16.1.6",
"opencc-js": "^1.0.5",
"react": "19.2.4",
@@ -31,8 +31,8 @@
"@types/react-dom": "^19",
"eslint": "^10",
"eslint-config-next": "16.1.6",
"postcss": "^8.5.6",
"postcss-preset-env": "^11.1.3",
"postcss": "^8.5.8",
"postcss-preset-env": "^11.2.0",
"tailwindcss": "^4",
"typescript": "^5"
}