mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-13 07:43:43 +08:00
feat: add full source resolution probing and display in player UI
This commit is contained in:
@@ -62,6 +62,7 @@
|
||||
- **实时延迟监测**:可选实时显示各源的网络延迟
|
||||
- **清晰度标签**:自动解析并显示视频清晰度(4K/蓝光/1080P/720P/HD 等),方便快速分辨源质量
|
||||
- **实际分辨率检测**:播放视频时自动检测并显示实际视频分辨率(如 1920x1080),不依赖源标签,显示真实清晰度。分辨率标签 5 秒后自动隐藏,鼠标移动时重新显示
|
||||
- **全源分辨率探测**:播放器源列表中所有源均自动通过 m3u8 清单探测实际分辨率,显示精确的分辨率标签(1080P/720P/4K 等),不仅限于当前播放的源
|
||||
- **繁体中文搜索**:自动将繁体中文转换为简体中文进行搜索,确保繁体输入也能搜到结果
|
||||
- **源过滤**:支持按源和类型筛选搜索结果,源标签支持按类型分组显示,智能合并同名分类标签,展开/折叠状态持久化记忆
|
||||
- **多级标签**:搜索结果和播放器中显示源名称和内容类型双重标签
|
||||
|
||||
@@ -10,6 +10,7 @@ import { PlayerError } from '@/components/player/PlayerError';
|
||||
import { SourceInfo } from '@/components/player/EpisodeList';
|
||||
import type { VideoSource } from '@/lib/types';
|
||||
import type { VideoResolutionInfo } from '@/components/player/hooks/useVideoResolution';
|
||||
import { useResolutionProbe } from '@/lib/hooks/useResolutionProbe';
|
||||
import { useVideoPlayer } from '@/lib/hooks/useVideoPlayer';
|
||||
import { useHistory } from '@/lib/store/history-store';
|
||||
import { FavoritesSidebar } from '@/components/favorites/FavoritesSidebar';
|
||||
@@ -269,6 +270,12 @@ function PlayerContent() {
|
||||
// Track detected video resolution from the player
|
||||
const [detectedResolution, setDetectedResolution] = useState<VideoResolutionInfo | null>(null);
|
||||
|
||||
// Probe resolution for all grouped sources (not just the playing one)
|
||||
const probeList = useMemo(() => {
|
||||
return groupedSources.map(s => ({ id: s.id, source: s.source }));
|
||||
}, [groupedSources]);
|
||||
const { resolutions: sourceResolutions } = useResolutionProbe(probeList);
|
||||
|
||||
// Add initial history entry when video data is loaded
|
||||
useEffect(() => {
|
||||
if (videoData && playUrl && videoId) {
|
||||
@@ -431,6 +438,7 @@ function PlayerContent() {
|
||||
sources={groupedSources.length > 0 ? groupedSources : undefined}
|
||||
currentSource={currentSourceId || source || ''}
|
||||
currentResolution={detectedResolution}
|
||||
sourceResolutions={sourceResolutions}
|
||||
onSourceChange={(newSource) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('id', String(newSource.id));
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useKeyboardNavigation } from '@/lib/hooks/useKeyboardNavigation';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
import { extractQualityLabel } from '@/lib/utils/video';
|
||||
import type { VideoResolutionInfo } from './hooks/useVideoResolution';
|
||||
import type { ResolutionInfo } from '@/lib/hooks/useResolutionProbe';
|
||||
|
||||
interface Episode {
|
||||
name?: string;
|
||||
@@ -39,6 +40,8 @@ interface EpisodeListProps {
|
||||
onSourceChange?: (source: SourceInfo) => void;
|
||||
// Actual detected resolution for the current source
|
||||
currentResolution?: VideoResolutionInfo | null;
|
||||
// Probed resolutions for all sources (key: "source:id")
|
||||
sourceResolutions?: Record<string, ResolutionInfo | null>;
|
||||
}
|
||||
|
||||
export function EpisodeList({
|
||||
@@ -51,6 +54,7 @@ export function EpisodeList({
|
||||
currentSource,
|
||||
onSourceChange,
|
||||
currentResolution,
|
||||
sourceResolutions,
|
||||
}: EpisodeListProps) {
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
@@ -63,6 +67,22 @@ export function EpisodeList({
|
||||
|
||||
const showSourceSelector = sources && sources.length > 1 && onSourceChange;
|
||||
|
||||
// Helper: get best resolution badge for a source
|
||||
const getResBadge = useCallback((source: SourceInfo, isCurrent: boolean) => {
|
||||
// For current source, prefer actual detected resolution from video element
|
||||
if (isCurrent && currentResolution) {
|
||||
return { label: currentResolution.label, color: currentResolution.color };
|
||||
}
|
||||
// Check probed resolution from m3u8 manifest
|
||||
const probeKey = `${source.source}:${source.id}`;
|
||||
const probed = sourceResolutions?.[probeKey];
|
||||
if (probed) {
|
||||
return { label: probed.label, color: probed.color };
|
||||
}
|
||||
// Fall back to quality label parsed from remarks
|
||||
return extractQualityLabel(source.remarks) || null;
|
||||
}, [currentResolution, sourceResolutions]);
|
||||
|
||||
// Current source info
|
||||
const currentSourceInfo = useMemo(() => {
|
||||
if (!sources || !currentSource) return null;
|
||||
@@ -331,18 +351,10 @@ export function EpisodeList({
|
||||
<div className="font-medium text-sm truncate flex items-center gap-1.5">
|
||||
{source.sourceName || source.source}
|
||||
{(() => {
|
||||
// For the current source, prefer actual detected resolution
|
||||
if (isCurrent && currentResolution) {
|
||||
return (
|
||||
<span className={`inline-flex items-center px-1 py-0 rounded text-[9px] font-bold text-white ${currentResolution.color}`}>
|
||||
{currentResolution.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const qb = extractQualityLabel(source.remarks);
|
||||
return qb ? (
|
||||
<span className={`inline-flex items-center px-1 py-0 rounded text-[9px] font-bold text-white ${qb.color}`}>
|
||||
{qb.label}
|
||||
const badge = getResBadge(source, isCurrent);
|
||||
return badge ? (
|
||||
<span className={`inline-flex items-center px-1 py-0 rounded text-[9px] font-bold text-white ${badge.color}`}>
|
||||
{badge.label}
|
||||
</span>
|
||||
) : null;
|
||||
})()}
|
||||
@@ -419,18 +431,10 @@ export function EpisodeList({
|
||||
<div className="font-medium text-sm truncate flex items-center gap-1.5">
|
||||
{source.sourceName || source.source}
|
||||
{(() => {
|
||||
// For the current source, prefer actual detected resolution
|
||||
if (isCurrent && currentResolution) {
|
||||
return (
|
||||
<span className={`inline-flex items-center px-1 py-0 rounded text-[9px] font-bold text-white ${currentResolution.color}`}>
|
||||
{currentResolution.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const qb = extractQualityLabel(source.remarks);
|
||||
return qb ? (
|
||||
<span className={`inline-flex items-center px-1 py-0 rounded text-[9px] font-bold text-white ${qb.color}`}>
|
||||
{qb.label}
|
||||
const badge = getResBadge(source, isCurrent);
|
||||
return badge ? (
|
||||
<span className={`inline-flex items-center px-1 py-0 rounded text-[9px] font-bold text-white ${badge.color}`}>
|
||||
{badge.label}
|
||||
</span>
|
||||
) : null;
|
||||
})()}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "4.8.0",
|
||||
"version": "4.8.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kvideo",
|
||||
"version": "4.8.0",
|
||||
"version": "4.8.1",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "4.8.0",
|
||||
"version": "4.8.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port ${PORT:-3000}",
|
||||
|
||||
Reference in New Issue
Block a user