杜比HDR

This commit is contained in:
Developer
2026-03-19 16:38:18 +08:00
parent 462a090599
commit 46796d3d6d
5 changed files with 38 additions and 10 deletions

View File

@@ -407,6 +407,14 @@ export const NativeVideoPlayer = forwardRef<NativeVideoPlayerRef, Props>(
videoRef.current?.seek(initialTime); videoRef.current?.seek(initialTime);
} }
}} }}
onError={(e) => {
// 杜比视界播放失败时自动降级到 1080P
if (currentQn === 126) {
onQualityChange(80);
return;
}
console.warn('Video playback error:', e);
}}
/> />
) : ( ) : (
<View style={styles.placeholder} /> <View style={styles.placeholder} />
@@ -572,7 +580,7 @@ export const NativeVideoPlayer = forwardRef<NativeVideoPlayerRef, Props>(
q.qn === currentQn && styles.qualityItemActive, q.qn === currentQn && styles.qualityItemActive,
]} ]}
> >
{q.desc} {q.desc}{q.qn === 126 ? ' DV' : ''}
</Text> </Text>
{q.qn === currentQn && ( {q.qn === currentQn && (
<Ionicons name="checkmark" size={16} color="#00AEEC" /> <Ionicons name="checkmark" size={16} color="#00AEEC" />

View File

@@ -39,7 +39,7 @@ export function useVideoDetail(bvid: string) {
setVideo(detail); setVideo(detail);
const cid = detail.pages?.[0]?.cid ?? detail.cid as number; const cid = detail.pages?.[0]?.cid ?? detail.cid as number;
cidRef.current = cid; cidRef.current = cid;
await fetchPlayData(cid, 120, true); await fetchPlayData(cid, 126, true);
} catch (e: any) { } catch (e: any) {
setError(e.message ?? 'Load failed'); setError(e.message ?? 'Load failed');
} finally { } finally {
@@ -52,7 +52,7 @@ export function useVideoDetail(bvid: string) {
// 登录状态变化时重新拉取清晰度列表(登录后可能获得更高画质) // 登录状态变化时重新拉取清晰度列表(登录后可能获得更高画质)
useEffect(() => { useEffect(() => {
if (cidRef.current) { 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); console.warn('Failed to refresh quality list after login change:', e);
}); });
} }

View File

@@ -116,8 +116,10 @@ export async function getVideoDetail(bvid: string): Promise<VideoItem> {
export async function getPlayUrl(bvid: string, cid: number, qn = 64): Promise<PlayUrlResponse> { export async function getPlayUrl(bvid: string, cid: number, qn = 64): Promise<PlayUrlResponse> {
const isAndroid = Platform.OS === 'android'; 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 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 }; : { bvid, cid, qn, fnval: 0, platform: 'html5', fourk: 1 };
const res = await api.get('/x/player/playurl', { params }); const res = await api.get('/x/player/playurl', { params });
return res.data.data as PlayUrlResponse; return res.data.data as PlayUrlResponse;

View File

@@ -69,6 +69,8 @@ export interface DashVideoItem {
stat:any; stat:any;
frameRate: string; frameRate: string;
segment_base?: DashSegmentBase; segment_base?: DashSegmentBase;
dolby_type?: number; // 1=杜比视界
hdr_type?: number; // 1=HDR
} }
export interface DashAudioItem { export interface DashAudioItem {
@@ -91,6 +93,10 @@ export interface PlayUrlResponse {
video: DashVideoItem[]; video: DashVideoItem[];
audio: DashAudioItem[]; audio: DashAudioItem[];
}; };
dolby?: {
type: number; // 1=杜比全景声
audio?: DashAudioItem[];
};
quality: number; quality: number;
accept_quality: number[]; accept_quality: number[];
accept_description: string[]; accept_description: string[];

View File

@@ -1,5 +1,5 @@
import * as FileSystem from 'expo-file-system/legacy'; 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 播放。 * 将 Bilibili DASH 响应写成 MPD 文件,返回 file:// URI 供 ExoPlayer 播放。
@@ -12,13 +12,20 @@ export async function buildDashMpdUri(playData: PlayUrlResponse, qn: number): Pr
return path; return path;
} }
function isDolbyVision(codecs: string): boolean {
return /^(dvhe|dvh1)/.test(codecs);
}
function buildMpdXml(playData: PlayUrlResponse, qn: number): string { function buildMpdXml(playData: PlayUrlResponse, qn: number): string {
const dash = playData.dash!; const dash = playData.dash!;
const video = dash.video.find(v => v.id === qn) ?? dash.video[0]; 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 dur = dash.duration;
const vSeg = video.segment_base; const vSeg = video.segment_base;
@@ -31,13 +38,18 @@ function buildMpdXml(playData: PlayUrlResponse, qn: number): string {
? `\n <SegmentBase indexRange="${aSeg.index_range}"><Initialization range="${aSeg.initialization}"/></SegmentBase>` ? `\n <SegmentBase indexRange="${aSeg.index_range}"><Initialization range="${aSeg.initialization}"/></SegmentBase>`
: ''; : '';
const isDV = isDolbyVision(video.codecs);
const dvProperty = isDV
? `\n <SupplementalProperty schemeIdUri="tag:dolby.com,2016:dash:dolby_vision_profile:2014" value="${video.codecs}"/>`
: '';
return `<?xml version="1.0" encoding="UTF-8"?> return `<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" <MPD xmlns="urn:mpeg:dash:schema:mpd:2011"
profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011,http://dashif.org/guidelines/dash-if-simple"
type="static" type="static"
mediaPresentationDuration="PT${dur}S"> mediaPresentationDuration="PT${dur}S">
<Period duration="PT${dur}S"> <Period duration="PT${dur}S">
<AdaptationSet id="1" mimeType="${video.mimeType}" codecs="${video.codecs}" startWithSAP="1" subsegmentAlignment="true"> <AdaptationSet id="1" mimeType="${video.mimeType}" codecs="${video.codecs}" startWithSAP="1" subsegmentAlignment="true">${dvProperty}
<Representation id="v1" bandwidth="${video.bandwidth}" width="${video.width}" height="${video.height}" frameRate="${video.frameRate}"> <Representation id="v1" bandwidth="${video.bandwidth}" width="${video.width}" height="${video.height}" frameRate="${video.frameRate}">
<BaseURL>${escapeXml(video.baseUrl)}</BaseURL>${videoSegmentBase} <BaseURL>${escapeXml(video.baseUrl)}</BaseURL>${videoSegmentBase}
</Representation> </Representation>