feat: unlock 1080P+ on Android via DASH streaming

- getPlayUrl uses fnval=16 (DASH) on Android, keeping fnval=0/html5 for iOS/web
- New utils/dash.ts builds a valid DASH MPD from Bilibili's segmentBase ranges
  and returns it as a data: URI for ExoPlayer consumption
- NativeVideoPlayer selects DASH path (type='mpd') or durl fallback automatically
- Extend PlayUrlResponse types with DashVideoItem/DashAudioItem/DashSegmentBase
This commit is contained in:
Developer
2026-03-10 20:04:48 +08:00
parent 4dbb3cb3d6
commit 5bb6a3cd68
8 changed files with 453 additions and 113 deletions

View File

@@ -1,7 +1,7 @@
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform } from 'react-native';
import type { VideoItem, Comment, PlayUrlResponse, QRCodeInfo } from './types';
import type { VideoItem, Comment, PlayUrlResponse, QRCodeInfo, VideoShotData, HeatmapResponse } from './types';
import { signWbi } from '../utils/wbi';
const isWeb = Platform.OS === 'web';
@@ -95,10 +95,11 @@ export async function getVideoDetail(bvid: string): Promise<VideoItem> {
}
export async function getPlayUrl(bvid: string, cid: number, qn = 64): Promise<PlayUrlResponse> {
const fnval = qn >= 80 ? 16 : 0;
const res = await api.get('/x/player/playurl', {
params: { bvid, cid, qn, fnval, platform: 'html5', fourk: 1 },
});
const isAndroid = Platform.OS === 'android';
const params = isAndroid
? { bvid, cid, qn, fnval: 16, 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;
}
@@ -115,6 +116,22 @@ export async function getComments(aid: number, pn = 1): Promise<Comment[]> {
return (res.data.data?.replies ?? []) as Comment[];
}
export async function getHeatmap(bvid: string): Promise<HeatmapResponse | null> {
try {
const res = await api.get('/pbp/data', { params: { bvid } });
return res.data.data as HeatmapResponse;
} catch { return null; }
}
export async function getVideoShot(bvid: string, cid: number): Promise<VideoShotData | null> {
try {
const res = await api.get('/x/player/videoshot', {
params: { bvid, cid, index: 1 },
});
return res.data.data as VideoShotData;
} catch { return null; }
}
export async function generateQRCode(): Promise<QRCodeInfo> {
const headers = isWeb
? {}

View File

@@ -34,6 +34,32 @@ export interface Comment {
replies: Comment[] | null;
}
export interface DashSegmentBase {
Initialization: string;
indexRange: string;
}
export interface DashVideoItem {
id: number;
baseUrl: string;
bandwidth: number;
mimeType: string;
codecs: string;
width: number;
height: number;
frameRate: string;
segmentBase: DashSegmentBase;
}
export interface DashAudioItem {
id: number;
baseUrl: string;
bandwidth: number;
mimeType: string;
codecs: string;
segmentBase: DashSegmentBase;
}
export interface PlayUrlResponse {
durl?: Array<{
url: string;
@@ -41,8 +67,9 @@ export interface PlayUrlResponse {
size: number;
}>;
dash?: {
video: Array<{ id: number; baseUrl: string; codecs: string; bandwidth: number }>;
audio: Array<{ id: number; baseUrl: string; codecs: string; bandwidth: number }>;
duration: number;
video: DashVideoItem[];
audio: DashAudioItem[];
};
quality: number;
accept_quality: number[];
@@ -53,3 +80,16 @@ export interface QRCodeInfo {
url: string;
qrcode_key: string;
}
export interface VideoShotData {
img_x_len: number;
img_y_len: number;
img_x_size: number;
img_y_size: number;
image: string[];
}
export interface HeatmapResponse {
timestamp: number;
pb_data: string;
}