0x00 开篇
最近遇到在使用 tauri
时遇到一个问题,当我在 tauri
中播放某些 m3u8
文件时会出现跨域问题。为解决这个问题,只能亲自操刀写一个简单的代理服务器了。
0x01 实现原理
代理的原理很简单,原本我们直接将m3u8
的地址传给播放器,而现在我们需要通过网络请求获取数据,然后将获取后的再通过本地代理服务器完整的转发一次,将代理后的地址再传给播放器。如下图所示:
假设,目前我请求的 m3u8 地址是 https://test.m3u8
。那通过代理后的地址就会变为http://127.0.0.1:25001/m3u8/https%3A%2F%2Ftest.m3u8
。我们将代理后的地址放入播放器,可以正常播放,就是我们最终实现的效果。
0x02 代码实现
由于代码很简单,我最终决定使用 warp
来实现。首先我们需要通过网络请求先获取到 m3u8
返回的内容:
let client = get_default_http_client();
let result = client.get(&url).send().await;
if result.is_err() {
return Ok(Response::builder()
.status(500).body("".to_string()).unwrap());
}
let content = result.unwrap().text().await;
let m3u8;
if content.is_err() {
m3u8 = "".to_string();
} else {
// m3u8 = content.unwrap();
m3u8 = process_m3u8(&url, content.unwrap());
}
如果你了解 m3u8
文件,那其实最常见的 m3u8
有两种类型,一种是媒体列表,会包含多个 m3u8
地址,另一种是 ts
分片文件。
返回媒体列表
如果返回媒体列表,由于地址也大都是 m3u8
地址,我们需要将里面的地址,再次替换为代理地址。如果包含音频地址,则还需要处理音频的代理。
if let Ok(Playlist::MasterPlaylist(mut pl)) = m3u8_rs::parse_playlist_res(content.as_bytes()) {
// println!("{:?}", pl.alternatives);
let path_prefix = format!("{}:{}/m3u8/", HOST, PORT);
// process audio
pl.alternatives.iter_mut().for_each(|mut media| {
if media.media_type == Audio && media.uri.is_some() {
if media.uri.as_ref().unwrap().starts_with("http") {
media.uri = Some(format!("{}{}", path_prefix, encode(media.uri.as_ref().unwrap())));
} else {
if let Some(position) = m3u8_path.rfind("/") {
let url = &m3u8_path[..position + 1];
let real_url = format!("{}{}", url, media.uri.as_ref().unwrap());
media.uri = Some(format!("{}{}", &path_prefix, encode(&real_url)));
}
}
}
});
// process m3u8 list
pl.variants.iter_mut().for_each(|mut variant| {
// let path_prefix = format!("{}:{}/m3u8/", HOST, PORT);
if variant.uri.starts_with("http") {
variant.uri = format!("{}{}", path_prefix, encode(&variant.uri));
} else {
if let Some(position) = m3u8_path.rfind("/") {
let url = &m3u8_path[..position + 1];
let real_url = format!("{}{}", url, &variant.uri);
variant.uri = format!("{}{}", &path_prefix, encode(&real_url));
}
}
});
let mut v: Vec<u8> = Vec::new();
if let Ok(_) = pl.write_to(&mut v) {
return String::from_utf8(v).unwrap();
}
}
返回ts分片文件
如果返回 ts 分片列表,那我们还需再次代理一次(先网络请求数据,再完整转发数据)。这个 ts
分片将是最终的播放地址。
if let Ok(Playlist::MediaPlaylist(mut pl)) = m3u8_rs::parse_playlist_res(content.as_bytes()) {
pl.segments.iter_mut().for_each(|segment| {
let path_prefix = format!("{}:{}/ts/", HOST, PORT);
if segment.uri.starts_with("http") {
segment.uri = format!("{}{}", path_prefix, encode(&segment.uri));
} else {
if let Some(position) = m3u8_path.rfind("/") {
let url = &m3u8_path[..position + 1];
let real_url = format!("{}{}", url, &segment.uri);
segment.uri = format!("{}{}", path_prefix, encode(&real_url));
}
}
});
// dbg!(&pl);
let mut v: Vec<u8> = Vec::new();
if let Ok(_) = pl.write_to(&mut v) {
return String::from_utf8(v).unwrap();
}
}
ts
和 m3u8
的代理实现原理基本是类似的。
/// proxy ts
async fn get_ts_content_async(ts: String) -> Result<impl warp::Reply, Infallible> {
let client = get_default_http_client();
let result = client.get(&ts).send().await;
if result.is_err() {
return Ok(Response::builder()
.status(500)
.header("Content-Type", "video/mp2t")
.body(vec![])
.unwrap());
}
let response = result.unwrap();
let headers_map = response.headers().clone();
let mut builder = Response::builder();
let headers = builder.headers_mut().unwrap();
for (k, v) in headers_map.into_iter() {
let h = k.unwrap();
if h != "content-length" {
headers.insert(h, HeaderValue::from_str(v.to_str().unwrap()).unwrap());
}
}
let content = response.bytes().await;
let ts = content.unwrap();
let res = builder
.status(200)
.body(ts.to_vec())
.unwrap();
Ok(res)
}
最后,添加路由地址,务必要添加跨域支持:
let cors = warp::cors().allow_any_origin();
// proxy m3u8
let m3u8_proxy_router = warp::path!("m3u8" / String).and_then(move |url: String| {
let decoded = decode(url.as_str()).expect("UTF-8");
// dbg!(&decoded);
get_m3u8_content_async(decoded.to_string())
}).with(&cors);
// proxy ts
let ts_proxy_router = warp::path!("ts" / String).and_then(move |url: String| {
let decoded = decode(url.as_str()).expect("UTF-8");
// dbg!(&decoded);
get_ts_content_async(decoded.to_string())
}).with(&cors);
let routers = m3u8_proxy_router.or(ts_proxy_router);
warp::serve(routers).run(([127, 0, 0, 1], 25011)).await;
0x03 最终效果
随便找了个地址,发现已经代理成功了。
0x04 小结
本文仅仅是简单的实现了一个代理功能,还有很多场景并没有考虑到,可能某些地址还需要验证信息等无法代理。