diff --git a/components/NativeVideoPlayer.tsx b/components/NativeVideoPlayer.tsx index 7841c2a..d9890c1 100644 --- a/components/NativeVideoPlayer.tsx +++ b/components/NativeVideoPlayer.tsx @@ -407,6 +407,14 @@ export const NativeVideoPlayer = forwardRef( videoRef.current?.seek(initialTime); } }} + onError={(e) => { + // 杜比视界播放失败时自动降级到 1080P + if (currentQn === 126) { + onQualityChange(80); + return; + } + console.warn('Video playback error:', e); + }} /> ) : ( @@ -572,7 +580,7 @@ export const NativeVideoPlayer = forwardRef( q.qn === currentQn && styles.qualityItemActive, ]} > - {q.desc} + {q.desc}{q.qn === 126 ? ' DV' : ''} {q.qn === currentQn && ( diff --git a/hooks/useVideoDetail.ts b/hooks/useVideoDetail.ts index 36ec4ec..7bf0386 100644 --- a/hooks/useVideoDetail.ts +++ b/hooks/useVideoDetail.ts @@ -39,7 +39,7 @@ export function useVideoDetail(bvid: string) { setVideo(detail); const cid = detail.pages?.[0]?.cid ?? detail.cid as number; cidRef.current = cid; - await fetchPlayData(cid, 120, true); + await fetchPlayData(cid, 126, true); } catch (e: any) { setError(e.message ?? 'Load failed'); } finally { @@ -52,7 +52,7 @@ export function useVideoDetail(bvid: string) { // 登录状态变化时重新拉取清晰度列表(登录后可能获得更高画质) useEffect(() => { if (cidRef.current) { - fetchPlayData(cidRef.current, 120, true).catch((e) => { + fetchPlayData(cidRef.current, 126, true).catch((e) => { console.warn('Failed to refresh quality list after login change:', e); }); } diff --git a/services/bilibili.ts b/services/bilibili.ts index 489031b..f16f139 100644 --- a/services/bilibili.ts +++ b/services/bilibili.ts @@ -116,8 +116,10 @@ export async function getVideoDetail(bvid: string): Promise { export async function getPlayUrl(bvid: string, cid: number, qn = 64): Promise { const isAndroid = Platform.OS === 'android'; + // 1488 = 16(DASH)|64(HDR)|128(4K)|256(杜比全景声)|1024(杜比视界) + const FNVAL_ANDROID = 16 | 64 | 128 | 256 | 1024; const params = isAndroid - ? { bvid, cid, qn, fnval: 16, fourk: 1 } + ? { bvid, cid, qn, fnval: FNVAL_ANDROID, fourk: 1 } : { bvid, cid, qn, fnval: 0, platform: 'html5', fourk: 1 }; const res = await api.get('/x/player/playurl', { params }); return res.data.data as PlayUrlResponse; diff --git a/services/types.ts b/services/types.ts index 1589e23..445b4df 100644 --- a/services/types.ts +++ b/services/types.ts @@ -69,6 +69,8 @@ export interface DashVideoItem { stat:any; frameRate: string; segment_base?: DashSegmentBase; + dolby_type?: number; // 1=杜比视界 + hdr_type?: number; // 1=HDR } export interface DashAudioItem { @@ -91,6 +93,10 @@ export interface PlayUrlResponse { video: DashVideoItem[]; audio: DashAudioItem[]; }; + dolby?: { + type: number; // 1=杜比全景声 + audio?: DashAudioItem[]; + }; quality: number; accept_quality: number[]; accept_description: string[]; diff --git a/utils/dash.ts b/utils/dash.ts index a0940cc..a5243f8 100644 --- a/utils/dash.ts +++ b/utils/dash.ts @@ -1,5 +1,5 @@ import * as FileSystem from 'expo-file-system/legacy'; -import type { PlayUrlResponse } from '../services/types'; +import type { PlayUrlResponse, DashAudioItem } from '../services/types'; /** * 将 Bilibili DASH 响应写成 MPD 文件,返回 file:// URI 供 ExoPlayer 播放。 @@ -12,13 +12,20 @@ export async function buildDashMpdUri(playData: PlayUrlResponse, qn: number): Pr return path; } +function isDolbyVision(codecs: string): boolean { + return /^(dvhe|dvh1)/.test(codecs); +} + function buildMpdXml(playData: PlayUrlResponse, qn: number): string { const dash = playData.dash!; const video = dash.video.find(v => v.id === qn) ?? dash.video[0]; - const audio = dash.audio.reduce((best, a) => - a.bandwidth > best.bandwidth ? a : best - ); + + // 优先使用杜比全景声音轨,回退到带宽最高的普通音轨 + const dolbyAudios = playData.dolby?.audio; + const audio: DashAudioItem = (dolbyAudios && dolbyAudios.length > 0) + ? dolbyAudios.reduce((best, a) => a.bandwidth > best.bandwidth ? a : best) + : dash.audio.reduce((best, a) => a.bandwidth > best.bandwidth ? a : best); const dur = dash.duration; const vSeg = video.segment_base; @@ -31,13 +38,18 @@ function buildMpdXml(playData: PlayUrlResponse, qn: number): string { ? `\n ` : ''; + const isDV = isDolbyVision(video.codecs); + const dvProperty = isDV + ? `\n ` + : ''; + return ` - + ${dvProperty} ${escapeXml(video.baseUrl)}${videoSegmentBase}