feat: 调整并重构视频音频流的选择逻辑,应该可以提升些许性能 (#212)

* feat: 调整并重构视频音频流的选择逻辑,应该可以提升些许性能

* test: 添加少量单元测试
This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-01-13 13:51:16 +08:00
committed by GitHub
parent 05aa30119e
commit 7d9999d6aa
+119 -106
View File
@@ -20,7 +20,8 @@ pub enum VideoQuality {
QualityDolby = 126, QualityDolby = 126,
Quality8k = 127, Quality8k = 127,
} }
#[derive(Debug, strum::FromRepr, PartialEq, PartialOrd, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, strum::FromRepr, PartialEq, Serialize, Deserialize)]
pub enum AudioQuality { pub enum AudioQuality {
Quality64k = 30216, Quality64k = 30216,
Quality132k = 30232, Quality132k = 30232,
@@ -29,8 +30,25 @@ pub enum AudioQuality {
Quality192k = 30280, Quality192k = 30280,
} }
impl AudioQuality {
#[inline]
pub fn as_sort_key(&self) -> isize {
match self {
// 这可以让 Dolby 和 Hi-RES 排在 192k 之后,且 Dolby 和 Hi-RES 之间的顺序不变
Self::QualityHiRES | Self::QualityDolby => (*self as isize) + 40,
_ => *self as isize,
}
}
}
impl PartialOrd<AudioQuality> for AudioQuality {
fn partial_cmp(&self, other: &AudioQuality) -> Option<std::cmp::Ordering> {
self.as_sort_key().partial_cmp(&other.as_sort_key())
}
}
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
#[derive(Debug, strum::EnumString, strum::Display, PartialEq, PartialOrd, Serialize, Deserialize)] #[derive(Debug, strum::EnumString, strum::Display, strum::AsRefStr, PartialEq, PartialOrd, Serialize, Deserialize)]
pub enum VideoCodecs { pub enum VideoCodecs {
#[strum(serialize = "hev")] #[strum(serialize = "hev")]
HEV, HEV,
@@ -115,26 +133,22 @@ impl PageAnalyzer {
} }
fn is_flv_stream(&self) -> bool { fn is_flv_stream(&self) -> bool {
self.info.get("durl").is_some() self.info.get("durl").is_some() && self.info["format"].as_str().is_some_and(|f| f.starts_with("flv"))
&& self.info["format"].is_string()
&& self.info["format"].as_str().unwrap().starts_with("flv")
} }
fn is_html5_mp4_stream(&self) -> bool { fn is_html5_mp4_stream(&self) -> bool {
self.info.get("durl").is_some() self.info.get("durl").is_some()
&& self.info["format"].is_string() && self.info["format"].as_str().is_some_and(|f| f.starts_with("mp4"))
&& self.info["format"].as_str().unwrap().starts_with("mp4") && self.info["is_html5"].as_bool().is_some_and(|b| b)
&& self.info["is_html5"].is_boolean()
&& self.info["is_html5"].as_bool().unwrap()
} }
fn is_episode_try_mp4_stream(&self) -> bool { fn is_episode_try_mp4_stream(&self) -> bool {
self.info.get("durl").is_some() self.info.get("durl").is_some()
&& self.info["format"].is_string() && self.info["format"].as_str().is_some_and(|f| f.starts_with("mp4"))
&& self.info["format"].as_str().unwrap().starts_with("mp4") && self.info["is_html5"].as_bool().is_none_or(|b| !b)
&& !(self.info["is_html5"].is_boolean() && self.info["is_html5"].as_bool().unwrap())
} }
/// 获取所有的视频、音频流,并根据条件筛选
fn streams(&mut self, filter_option: &FilterOption) -> Result<Vec<Stream>> { fn streams(&mut self, filter_option: &FilterOption) -> Result<Vec<Stream>> {
if self.is_flv_stream() { if self.is_flv_stream() {
return Ok(vec![Stream::Flv( return Ok(vec![Stream::Flv(
@@ -161,85 +175,78 @@ impl PageAnalyzer {
)]); )]);
} }
let mut streams: Vec<Stream> = Vec::new(); let mut streams: Vec<Stream> = Vec::new();
let videos_data = self.info["dash"]["video"].take(); for video in self.info["dash"]["video"]
let audios_data = self.info["dash"]["audio"].take(); .as_array()
let flac_data = self.info["dash"]["flac"].take(); .ok_or(BiliError::RiskControlOccurred)?
let dolby_data = self.info["dash"]["dolby"].take(); .iter()
for video_data in videos_data.as_array().ok_or(BiliError::RiskControlOccurred)?.iter() {
let video_stream_url = video_data["baseUrl"].as_str().unwrap().to_string();
let video_stream_quality = VideoQuality::from_repr(video_data["id"].as_u64().unwrap() as usize)
.ok_or(anyhow!("invalid video stream quality"))?;
if (video_stream_quality == VideoQuality::QualityHdr && filter_option.no_hdr)
|| (video_stream_quality == VideoQuality::QualityDolby && filter_option.no_dolby_video)
|| (video_stream_quality != VideoQuality::QualityDolby
&& video_stream_quality != VideoQuality::QualityHdr
&& (video_stream_quality < filter_option.video_min_quality
|| video_stream_quality > filter_option.video_max_quality))
// 此处过滤包含三种情况:
// 1. HDR 视频,但指定不需要 HDR
// 2. 杜比视界视频,但指定不需要杜比视界
// 3. 视频质量不在指定范围内
{ {
continue; let (Some(url), Some(quality), Some(codecs)) = (
} video["baseUrl"].as_str(),
video["id"].as_u64(),
let video_codecs = video_data["codecs"].as_str().unwrap(); video["codecs"].as_str(),
// 从视频流的 codecs 字段中获取编码格式,此处并非精确匹配而是判断包含,比如 codecs 是 av1.42c01e,需要匹配为 av1 ) else {
let video_codecs = vec![VideoCodecs::HEV, VideoCodecs::AVC, VideoCodecs::AV1]
.into_iter()
.find(|c| video_codecs.contains(c.to_string().as_str()));
let Some(video_codecs) = video_codecs else {
continue; continue;
}; };
if !filter_option.codecs.contains(&video_codecs) { let quality = VideoQuality::from_repr(quality as usize).ok_or(anyhow!("invalid video stream quality"))?;
// 从视频流的 codecs 字段中获取编码格式,此处并非精确匹配而是判断包含,比如 codecs 是 av1.42c01e,需要匹配为 av1
let codecs = [VideoCodecs::HEV, VideoCodecs::AVC, VideoCodecs::AV1]
.into_iter()
.find(|c| codecs.contains(c.as_ref()))
.ok_or(anyhow!("invalid video stream codecs"))?;
if !filter_option.codecs.contains(&codecs)
|| quality < filter_option.video_min_quality
|| quality > filter_option.video_max_quality
|| (quality == VideoQuality::QualityHdr && filter_option.no_hdr)
|| (quality == VideoQuality::QualityDolby && filter_option.no_dolby_video)
{
continue; continue;
} }
streams.push(Stream::DashVideo { streams.push(Stream::DashVideo {
url: video_stream_url, url: url.to_string(),
quality: video_stream_quality, quality,
codecs: video_codecs, codecs,
}); });
} }
if audios_data.is_array() { if let Some(audios) = self.info["dash"]["audio"].as_array() {
for audio_data in audios_data.as_array().unwrap().iter() { for audio in audios.iter() {
let audio_stream_url = audio_data["baseUrl"].as_str().unwrap().to_string(); let (Some(url), Some(quality)) = (audio["baseUrl"].as_str(), audio["id"].as_u64()) else {
let audio_stream_quality = AudioQuality::from_repr(audio_data["id"].as_u64().unwrap() as usize);
let Some(audio_stream_quality) = audio_stream_quality else {
continue; continue;
}; };
if audio_stream_quality > filter_option.audio_max_quality let quality =
|| audio_stream_quality < filter_option.audio_min_quality AudioQuality::from_repr(quality as usize).ok_or(anyhow!("invalid audio stream quality"))?;
{ if quality < filter_option.audio_min_quality || quality > filter_option.audio_max_quality {
continue; continue;
} }
streams.push(Stream::DashAudio { streams.push(Stream::DashAudio {
url: audio_stream_url, url: url.to_string(),
quality: audio_stream_quality, quality,
}); });
} }
} }
if !(filter_option.no_hires || flac_data["audio"].is_null()) { let flac = &self.info["dash"]["flac"]["audio"];
// 允许 hires 且存在 flac 音频流才会进来 if !(filter_option.no_hires || flac.is_null()) {
let flac_stream_url = flac_data["audio"]["baseUrl"].as_str().unwrap().to_string(); let (Some(url), Some(quality)) = (flac["baseUrl"].as_str(), flac["id"].as_u64()) else {
let flac_stream_quality = bail!("invalid flac stream");
AudioQuality::from_repr(flac_data["audio"]["id"].as_u64().unwrap() as usize).unwrap(); };
let quality = AudioQuality::from_repr(quality as usize).ok_or(anyhow!("invalid flac stream quality"))?;
if quality >= filter_option.audio_min_quality && quality <= filter_option.audio_max_quality {
streams.push(Stream::DashAudio { streams.push(Stream::DashAudio {
url: flac_stream_url, url: url.to_string(),
quality: flac_stream_quality, quality,
}); });
} }
if !(filter_option.no_dolby_audio || dolby_data["audio"].is_null()) { }
// 同理,允许杜比音频且存在杜比音频流才会进来 let dolby_audio = &self.info["dash"]["dolby"]["audio"][0];
let dolby_stream_data = dolby_data["audio"].as_array().and_then(|v| v.first()); if !(filter_option.no_dolby_audio || dolby_audio.is_null()) {
if dolby_stream_data.is_some() { let (Some(url), Some(quality)) = (dolby_audio["baseUrl"].as_str(), dolby_audio["id"].as_u64()) else {
let dolby_stream_data = dolby_stream_data.unwrap(); bail!("invalid dolby audio stream");
let dolby_stream_url = dolby_stream_data["baseUrl"].as_str().unwrap().to_string(); };
let dolby_stream_quality = let quality =
AudioQuality::from_repr(dolby_stream_data["id"].as_u64().unwrap() as usize).unwrap(); AudioQuality::from_repr(quality as usize).ok_or(anyhow!("invalid dolby audio stream quality"))?;
if quality >= filter_option.audio_min_quality && quality <= filter_option.audio_max_quality {
streams.push(Stream::DashAudio { streams.push(Stream::DashAudio {
url: dolby_stream_url, url: url.to_string(),
quality: dolby_stream_quality, quality,
}); });
} }
} }
@@ -250,13 +257,14 @@ impl PageAnalyzer {
let streams = self.streams(filter_option)?; let streams = self.streams(filter_option)?;
if self.is_flv_stream() || self.is_html5_mp4_stream() || self.is_episode_try_mp4_stream() { if self.is_flv_stream() || self.is_html5_mp4_stream() || self.is_episode_try_mp4_stream() {
// 按照 streams 中的假设,符合这三种情况的流只有一个,直接取 // 按照 streams 中的假设,符合这三种情况的流只有一个,直接取
return Ok(BestStream::Mixed(streams.into_iter().next().unwrap())); return Ok(BestStream::Mixed(
streams.into_iter().next().ok_or(anyhow!("no stream found"))?,
));
} }
// 将视频流和音频流拆分,分别做排序 let (videos, audios): (Vec<Stream>, Vec<Stream>) =
let (mut video_streams, mut audio_streams): (Vec<_>, Vec<_>) =
streams.into_iter().partition(|s| matches!(s, Stream::DashVideo { .. })); streams.into_iter().partition(|s| matches!(s, Stream::DashVideo { .. }));
// 因为该处的排序与筛选选项有关,因此不能在外面实现 PartialOrd trait,只能在这里写闭包 Ok(BestStream::VideoAudio {
video_streams.sort_by(|a, b| match (a, b) { video: Iterator::max_by(videos.into_iter(), |a, b| match (a, b) {
( (
Stream::DashVideo { Stream::DashVideo {
quality: a_quality, quality: a_quality,
@@ -269,22 +277,9 @@ impl PageAnalyzer {
.. ..
}, },
) => { ) => {
if a_quality == &VideoQuality::QualityDolby && !filter_option.no_dolby_video {
return std::cmp::Ordering::Greater;
}
if b_quality == &VideoQuality::QualityDolby && !filter_option.no_dolby_video {
return std::cmp::Ordering::Less;
}
if a_quality == &VideoQuality::QualityHdr && !filter_option.no_hdr {
return std::cmp::Ordering::Greater;
}
if b_quality == &VideoQuality::QualityHdr && !filter_option.no_hdr {
return std::cmp::Ordering::Less;
}
if a_quality != b_quality { if a_quality != b_quality {
return a_quality.partial_cmp(b_quality).unwrap(); return a_quality.partial_cmp(b_quality).unwrap();
} };
// 如果视频质量相同,按照偏好的编码优先级排序
filter_option filter_option
.codecs .codecs
.iter() .iter()
@@ -292,26 +287,44 @@ impl PageAnalyzer {
.cmp(&filter_option.codecs.iter().position(|c| c == a_codecs)) .cmp(&filter_option.codecs.iter().position(|c| c == a_codecs))
} }
_ => unreachable!(), _ => unreachable!(),
}); })
audio_streams.sort_by(|a, b| match (a, b) { .ok_or(anyhow!("no video stream found"))?,
audio: Iterator::max_by(audios.into_iter(), |a, b| match (a, b) {
(Stream::DashAudio { quality: a_quality, .. }, Stream::DashAudio { quality: b_quality, .. }) => { (Stream::DashAudio { quality: a_quality, .. }, Stream::DashAudio { quality: b_quality, .. }) => {
if a_quality == &AudioQuality::QualityDolby && !filter_option.no_dolby_audio {
return std::cmp::Ordering::Greater;
}
if b_quality == &AudioQuality::QualityDolby && !filter_option.no_dolby_audio {
return std::cmp::Ordering::Less;
}
a_quality.partial_cmp(b_quality).unwrap() a_quality.partial_cmp(b_quality).unwrap()
} }
_ => unreachable!(), _ => unreachable!(),
}); }),
if video_streams.is_empty() {
bail!("no video stream found");
}
Ok(BestStream::VideoAudio {
video: video_streams.remove(video_streams.len() - 1),
// 音频流可能为空,因此直接使用 pop 返回 Option
audio: audio_streams.pop(),
}) })
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quality_order() {
assert!([
VideoQuality::Quality360p,
VideoQuality::Quality480p,
VideoQuality::Quality720p,
VideoQuality::Quality1080p,
VideoQuality::Quality1080pPLUS,
VideoQuality::Quality1080p60,
VideoQuality::Quality4k,
VideoQuality::QualityHdr,
VideoQuality::QualityDolby,
VideoQuality::Quality8k
]
.is_sorted());
assert!([
AudioQuality::Quality64k,
AudioQuality::Quality132k,
AudioQuality::Quality192k,
AudioQuality::QualityDolby,
AudioQuality::QualityHiRES,
]
.is_sorted());
}
}