Skip to main content

bpi_rs/video/
videostream_url.rs

1//! 视频流地址相关接口 (web端)
2//!
3//! [查看 API 文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/video)
4use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
5use serde::{ Deserialize, Serialize };
6
7// --- 视频流URL相关数据结构体 ---
8
9/// DASH 流信息
10#[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/// DASH 流中的 Dolby 音频信息
21#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct DashDolby {
23    pub r#type: u8,
24    pub audio: Option<Vec<DashStream>>,
25}
26
27/// DASH 流中的 FLAC 音频信息
28#[derive(Debug, Clone, Deserialize, Serialize)]
29pub struct DashFlac {
30    pub audio: Vec<DashStream>,
31}
32
33/// 单个 DASH 流信息
34#[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/// FLV/MP4 视频分段流信息
62#[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/// 支持的格式详细信息
74#[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/// 视频流URL响应数据
85#[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
107// --- API 实现 ---
108
109impl BpiClient {
110    /// 获取视频流地址(web端)
111    ///
112    /// # 文档
113    /// [查看API文档](https://socialsisteryi.github.io/bilibili-API-collect/docs/video/videostream_url.html#获取视频流地址)
114    ///
115    /// # 参数
116    /// | 名称         | 类型           | 说明                 |
117    /// | ------------ | --------------| -------------------- |
118    /// | `aid`        | `Option<u64>`   | 稿件 avid,可选      |
119    /// | `bvid`       | `Option<&str>`  | 稿件 bvid,可选      |
120    /// | `cid`        | u64           | 视频 cid             |
121    /// | `qn`         | `Option<u64>`   | 清晰度选择,可选     |
122    /// | `fnval`      | `Option<u64>`   | 流格式标识,可选,默认1(MP4) |
123    /// | `fnver`      | `Option<u64>`   | 流版本标识,可选,默认0 |
124    /// | `fourk`      | `Option<u8>`    | 是否允许4K,可选,默认0 |
125    /// | `platform`   | `Option<&str>`  | 平台标识,可选,默认"pc" |
126    /// | `high_quality`| `Option<u8>`   | 是否高画质,可选     |
127    /// | `try_look`   | `Option<u8>`    | 是否可不登录拉取高画质,可选 |
128    ///
129    /// `aid` 和 `bvid` 必须提供一个。
130    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        // 签名
180        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(&params)
186            .send_bpi("获取视频流地址").await
187    }
188}
189
190// --- 测试模块 ---
191
192#[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        // 请求 MP4 格式,720P
204        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        // 请求 4K
229        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}