1use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
5use serde::{ Deserialize, Serialize };
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct DashInfo {
12 pub video: Vec<DashStream>,
13 pub audio: Vec<DashStream>,
14 #[serde(rename = "dolby")]
15 pub dolby: Option<DashDolby>,
16 pub flac: Option<DashFlac>,
17 pub duration: u64,
18}
19
20#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct DashDolby {
23 pub r#type: u8,
24 pub audio: Option<Vec<DashStream>>,
25}
26
27#[derive(Debug, Clone, Deserialize, Serialize)]
29pub struct DashFlac {
30 pub audio: Vec<DashStream>,
31}
32
33#[derive(Debug, Clone, Deserialize, Serialize)]
35pub struct DashStream {
36 pub id: u64,
37 #[serde(rename = "baseUrl")]
38 pub base_url: String,
39
40 #[serde(rename = "backupUrl")]
41 pub backup_url: Vec<String>,
42 pub bandwidth: u64,
43 #[serde(rename = "mimeType")]
44 pub mime_type: String,
45 pub codecs: String,
46 pub width: Option<u32>,
47 pub height: Option<u32>,
48 #[serde(rename = "frameRate")]
49 pub frame_rate: Option<String>,
50 pub sar: Option<String>,
51 pub start_with_sap: Option<u8>,
52 pub segment_base: Option<serde_json::Value>,
53 pub md5: Option<String>,
54 pub size: Option<u64>,
55 pub db_type: Option<u8>,
56 pub r#type: Option<String>,
57 pub stream_name: Option<String>,
58 pub orientation: Option<u8>,
59}
60
61#[derive(Debug, Clone, Deserialize, Serialize)]
63pub struct DurlInfo {
64 pub order: u32,
65 pub length: u64,
66 pub size: u64,
67 pub ahead: String,
68 pub vhead: String,
69 pub url: String,
70 pub backup_url: Vec<String>,
71}
72
73#[derive(Debug, Clone, Deserialize, Serialize)]
75pub struct SupportFormat {
76 pub quality: u64,
77 pub format: String,
78 pub new_description: String,
79 pub display_desc: String,
80 pub superscript: String,
81 pub codecs: Option<Vec<String>>,
82}
83
84#[derive(Debug, Clone, Deserialize, Serialize)]
86pub struct PlayUrlResponseData {
87 pub from: String,
88 pub result: String,
89 pub message: String,
90 pub quality: u64,
91 pub format: String,
92 pub timelength: u64,
93 pub accept_format: String,
94 pub accept_description: Vec<String>,
95 pub accept_quality: Vec<u64>,
96 pub video_codecid: u8,
97 pub seek_param: String,
98 pub seek_type: String,
99 pub durl: Option<Vec<DurlInfo>>,
100 pub dash: Option<DashInfo>,
101 pub support_formats: Vec<SupportFormat>,
102 pub high_format: Option<serde_json::Value>,
103 pub last_play_time: u64,
104 pub last_play_cid: u64,
105}
106
107impl BpiClient {
110 pub async fn video_playurl(
131 &self,
132 aid: Option<u64>,
133 bvid: Option<&str>,
134 cid: u64,
135 qn: Option<u64>,
136 fnval: Option<u64>,
137 fnver: Option<u64>,
138 fourk: Option<u8>,
139 platform: Option<&str>,
140 high_quality: Option<u8>,
141 try_look: Option<u8>
142 ) -> Result<BpiResponse<PlayUrlResponseData>, BpiError> {
143 if aid.is_none() && bvid.is_none() {
144 return Err(BpiError::parse("必须提供 aid 或 bvid"));
145 }
146
147 let mut params = vec![("cid", cid.to_string())];
148
149 if let Some(a) = aid {
150 params.push(("avid", a.to_string()));
151 }
152 if let Some(b) = bvid {
153 params.push(("bvid", b.to_string()));
154 }
155 if let Some(q) = qn {
156 params.push(("qn", q.to_string()));
157 }
158 if let Some(f) = fnval {
159 params.push(("fnval", f.to_string()));
160 }
161 if let Some(f) = fnver {
162 params.push(("fnver", f.to_string()));
163 }
164 if let Some(f) = fourk {
165 params.push(("fourk", f.to_string()));
166 }
167 if let Some(p) = platform {
168 params.push(("platform", p.to_string()));
169 } else {
170 params.push(("platform", "pc".to_string()));
171 }
172 if let Some(h) = high_quality {
173 params.push(("high_quality", h.to_string()));
174 }
175 if let Some(t) = try_look {
176 params.push(("try_look", t.to_string()));
177 }
178
179 let params = self.get_wbi_sign2(params).await?;
181
182 self
183 .get("https://api.bilibili.com/x/player/wbi/playurl")
184 .with_bilibili_headers()
185 .query(¶ms)
186 .send_bpi("获取视频流地址").await
187 }
188}
189
190#[cfg(test)]
193mod tests {
194 use super::*;
195 use tracing::info;
196
197 const TEST_AID: u64 = 113898824998659;
198 const TEST_CID: u64 = 28104724389;
199
200 #[tokio::test]
201 async fn test_video_playurl_mp4_by_aid() -> Result<(), BpiError> {
202 let bpi = BpiClient::new();
203 let resp = bpi.video_playurl(
205 Some(TEST_AID),
206 None,
207 TEST_CID,
208 Some(64),
209 Some(1),
210 None,
211 None,
212 None,
213 None,
214 None
215 ).await?;
216 let data = resp.into_data()?;
217
218 info!("MP4 视频流信息: {:?}", data);
219 assert!(!data.durl.is_none());
220 assert_eq!(data.quality, 64);
221
222 Ok(())
223 }
224
225 #[tokio::test]
226 async fn test_video_playurl_4k() -> Result<(), BpiError> {
227 let bpi = BpiClient::new();
228 let resp = bpi.video_playurl(
230 Some(TEST_AID),
231 None,
232 TEST_CID,
233 Some(120),
234 Some(16 | 128),
235 Some(0),
236 Some(1),
237 None,
238 None,
239 None
240 ).await?;
241 let data = resp.into_data()?;
242
243 info!("4K 视频流信息: {:?}", data);
244 assert!(!data.dash.is_none());
245 assert_eq!(data.quality, 120);
246
247 Ok(())
248 }
249}