mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
杜比HDR
This commit is contained in:
@@ -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" />
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user