Skip to main content

bpi_rs/live/
live_stream.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Serialize, Clone, Deserialize)]
4pub struct QualityDescription {
5    /// 画质代码
6    pub qn: i32,
7    /// 该代码对应的画质名称
8    pub desc: String,
9}
10
11#[derive(Debug, Serialize, Clone, Deserialize)]
12pub struct LiveStreamUrl {
13    /// 直播流url
14    pub url: String,
15    /// 服务器线路序号
16    pub order: i32,
17    /// 作用尚不明确
18    pub stream_type: i32,
19    /// 作用尚不明确
20    pub p2p_type: i32,
21}
22
23#[derive(Debug, Serialize, Clone, Deserialize)]
24pub struct LiveStreamData {
25    /// 当前画质代码qn
26    pub current_quality: i32,
27    /// 可选画质数参数
28    pub accept_quality: Vec<String>,
29    /// 当前画质代码quality
30    pub current_qn: i32,
31    /// 可选画质参数quality
32    pub quality_description: Vec<QualityDescription>,
33    /// 直播流url组
34    pub durl: Vec<LiveStreamUrl>,
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use crate::probe::contract::HttpMethod;
41    use crate::probe::endpoint_contract::EndpointContract;
42    use crate::{ApiEnvelope, BpiClient, BpiResult};
43
44    fn contract() -> BpiResult<EndpointContract> {
45        EndpointContract::from_slice(include_bytes!(
46            "../../tests/contracts/live/public-core/stream/contract.json"
47        ))
48    }
49
50    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
51    #[tokio::test]
52    async fn test_get_live_stream() {
53        let bpi = BpiClient::new().expect("client should build");
54        let data = bpi
55            .live()
56            .stream(14073662, Some("web"), None, Some(10000))
57            .await
58            .unwrap();
59        tracing::info!("{:?}", data);
60    }
61
62    #[test]
63    fn live_stream_contract_matches_endpoint_request() -> BpiResult<()> {
64        let contract = contract()?;
65
66        assert_eq!(contract.name, "live.stream");
67        assert_eq!(contract.request.method, HttpMethod::Get);
68        assert_eq!(
69            contract.request.url.as_str(),
70            "https://api.live.bilibili.com/room/v1/Room/playUrl"
71        );
72        assert_eq!(
73            contract.request.query.get("cid").map(String::as_str),
74            Some("14073662")
75        );
76        assert_eq!(
77            contract.request.query.get("platform").map(String::as_str),
78            Some("web")
79        );
80        assert_eq!(
81            contract.request.query.get("qn").map(String::as_str),
82            Some("10000")
83        );
84        assert_eq!(contract.cases.len(), 3);
85        assert_eq!(
86            contract.cases[0].response.rust_model.as_deref(),
87            Some("LiveStreamData")
88        );
89        Ok(())
90    }
91
92    #[test]
93    fn live_stream_response_fixture_parses_declared_model() -> BpiResult<()> {
94        let payload = ApiEnvelope::<LiveStreamData>::from_slice(include_bytes!(
95            "../../tests/contracts/live/public-core/stream/responses/success.json"
96        ))?
97        .into_payload()?;
98
99        assert_eq!(payload.current_qn, 10000);
100        assert_eq!(payload.durl.len(), 1);
101        Ok(())
102    }
103
104    fn local_probe_body(profile: &str) -> Option<serde_json::Value> {
105        let path = format!("target/bpi-probe-runs/live/public-core/stream/{profile}.response.json");
106        let bytes = std::fs::read(path).ok()?;
107        let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
108        value
109            .get("response")
110            .and_then(|response| response.get("body"))
111            .cloned()
112    }
113
114    #[test]
115    fn live_stream_model_matches_local_probe_outputs_when_available() -> BpiResult<()> {
116        for profile in ["anonymous", "normal", "vip"] {
117            if let Some(body) = local_probe_body(profile) {
118                let payload =
119                    serde_json::from_value::<ApiEnvelope<LiveStreamData>>(body)?.into_payload()?;
120                assert!(!payload.durl.is_empty());
121            }
122        }
123        Ok(())
124    }
125}