Rust实战 Rust简单实现m3u8代理

671 阅读3分钟

0x00 开篇

最近遇到在使用 tauri 时遇到一个问题,当我在 tauri 中播放某些 m3u8 文件时会出现跨域问题。为解决这个问题,只能亲自操刀写一个简单的代理服务器了。

0x01 实现原理

代理的原理很简单,原本我们直接将m3u8 的地址传给播放器,而现在我们需要通过网络请求获取数据,然后将获取后的再通过本地代理服务器完整的转发一次,将代理后的地址再传给播放器。如下图所示:

Untitled-2023-03-22-2150

假设,目前我请求的 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();
        }
    } 

tsm3u8的代理实现原理基本是类似的。

/// 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 最终效果

随便找了个地址,发现已经代理成功了。

image-20230507112259285

0x04 小结

本文仅仅是简单的实现了一个代理功能,还有很多场景并没有考虑到,可能某些地址还需要验证信息等无法代理。

本文源码:1595901624/iptv-proxy-rs: A local proxy service for m3u8 files built by rust. The purpose is to solve the CORS problems that occur when playing m3u8 files in electron, tari, walis and other frameworks (github.com)