bpi_rs/video/
videostream_url.rs

1//! 视频流地址相关接口 (web端)
2//!
3//! 文档: 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    /// 文档: https://socialsisteryi.github.io/bilibili-API-collect/docs/video/videostream_url.html#获取视频流地址
113    ///
114    /// # 参数
115    /// | 名称         | 类型           | 说明                 |
116    /// | ------------ | --------------| -------------------- |
117    /// | `aid`        | Option<u64>   | 稿件 avid,可选      |
118    /// | `bvid`       | Option<&str>  | 稿件 bvid,可选      |
119    /// | `cid`        | u64           | 视频 cid             |
120    /// | `qn`         | Option<u64>   | 清晰度选择,可选     |
121    /// | `fnval`      | Option<u64>   | 流格式标识,可选,默认1(MP4) |
122    /// | `fnver`      | Option<u64>   | 流版本标识,可选,默认0 |
123    /// | `fourk`      | Option<u8>    | 是否允许4K,可选,默认0 |
124    /// | `platform`   | Option<&str>  | 平台标识,可选,默认"pc" |
125    /// | `high_quality`| Option<u8>   | 是否高画质,可选     |
126    /// | `try_look`   | Option<u8>    | 是否可不登录拉取高画质,可选 |
127    ///
128    /// `aid` 和 `bvid` 必须提供一个。
129    pub async fn video_playurl(
130        &self,
131        aid: Option<u64>,
132        bvid: Option<&str>,
133        cid: u64,
134        qn: Option<u64>,
135        fnval: Option<u64>,
136        fnver: Option<u64>,
137        fourk: Option<u8>,
138        platform: Option<&str>,
139        high_quality: Option<u8>,
140        try_look: Option<u8>,
141    ) -> Result<BpiResponse<PlayUrlResponseData>, BpiError> {
142        if aid.is_none() && bvid.is_none() {
143            return Err(BpiError::parse("必须提供 aid 或 bvid"));
144        }
145
146        let mut params = vec![("cid", cid.to_string())];
147
148        if let Some(a) = aid {
149            params.push(("avid", a.to_string()));
150        }
151        if let Some(b) = bvid {
152            params.push(("bvid", b.to_string()));
153        }
154        if let Some(q) = qn {
155            params.push(("qn", q.to_string()));
156        }
157        if let Some(f) = fnval {
158            params.push(("fnval", f.to_string()));
159        }
160        if let Some(f) = fnver {
161            params.push(("fnver", f.to_string()));
162        }
163        if let Some(f) = fourk {
164            params.push(("fourk", f.to_string()));
165        }
166        if let Some(p) = platform {
167            params.push(("platform", p.to_string()));
168        } else {
169            params.push(("platform", "pc".to_string()));
170        }
171        if let Some(h) = high_quality {
172            params.push(("high_quality", h.to_string()));
173        }
174        if let Some(t) = try_look {
175            params.push(("try_look", t.to_string()));
176        }
177
178        // 签名
179        let params = self.get_wbi_sign2(params).await?;
180
181        self.get("https://api.bilibili.com/x/player/wbi/playurl")
182            .with_bilibili_headers()
183            .query(&params)
184            .send_bpi("获取视频流地址")
185            .await
186    }
187}
188
189// --- 测试模块 ---
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use tracing::info;
195
196    const TEST_AID: u64 = 113898824998659;
197    const TEST_CID: u64 = 28104724389;
198
199    #[tokio::test]
200
201    async fn test_video_playurl_mp4_by_aid() -> Result<(), BpiError> {
202        let bpi = BpiClient::new();
203        // 请求 MP4 格式,720P
204        let resp = bpi
205            .video_playurl(
206                Some(TEST_AID),
207                None,
208                TEST_CID,
209                Some(64),
210                Some(1),
211                None,
212                None,
213                None,
214                None,
215                None,
216            )
217            .await?;
218        let data = resp.into_data()?;
219
220        info!("MP4 视频流信息: {:?}", data);
221        assert!(!data.durl.is_none());
222        assert_eq!(data.quality, 64);
223
224        Ok(())
225    }
226
227    #[tokio::test]
228
229    async fn test_video_playurl_4k() -> Result<(), BpiError> {
230        let bpi = BpiClient::new();
231        // 请求 4K
232        let resp = bpi
233            .video_playurl(
234                Some(TEST_AID),
235                None,
236                TEST_CID,
237                Some(120),
238                Some(16 | 128),
239                Some(0),
240                Some(1),
241                None,
242                None,
243                None,
244            )
245            .await?;
246        let data = resp.into_data()?;
247
248        info!("4K 视频流信息: {:?}", data);
249        assert!(!data.dash.is_none());
250        assert_eq!(data.quality, 120);
251
252        Ok(())
253    }
254}