use fs_err as fs;
use std::env;
use std::time::Duration;
use std::process::Command;
use std::io::Cursor;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use axum::{routing::get, Router};
use axum::extract::State;
use axum::response::{Response, IntoResponse};
use axum::http::{header, StatusCode};
use axum::body::{Full, Bytes};
use axum_auth::AuthBasic;
use dash_mpd::{MPD, Period, AdaptationSet, Representation, SegmentTemplate};
use anyhow::{Context, Result};
use env_logger::Env;
use log::info;
#[derive(Debug, Default)]
struct AppState {
counter: AtomicUsize,
}
impl AppState {
fn new() -> AppState {
AppState { counter: AtomicUsize::new(0) }
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_basic_auth() -> Result<()> {
let shared_state = Arc::new(AppState::new());
async fn send_mpd(AuthBasic((id, password)): AuthBasic) -> impl IntoResponse {
info!("mpd request: auth {id:?}:{password:?}");
let segment_template = SegmentTemplate {
initialization: Some("/media/f1.mp4".to_string()),
..Default::default()
};
let rep = Representation {
id: Some("1".to_string()),
mimeType: Some("video/mp4".to_string()),
codecs: Some("avc1.640028".to_string()),
width: Some(1920),
height: Some(800),
bandwidth: Some(1980081),
SegmentTemplate: Some(segment_template),
..Default::default()
};
let adapt = AdaptationSet {
id: Some("1".to_string()),
contentType: Some("video".to_string()),
representations: vec!(rep),
..Default::default()
};
let period = Period {
id: Some("1".to_string()),
duration: Some(Duration::new(5, 0)),
adaptations: vec!(adapt.clone()),
..Default::default()
};
let mpd = MPD {
mpdtype: Some("static".to_string()),
periods: vec!(period),
..Default::default()
};
let xml = quick_xml::se::to_string(&mpd).unwrap();
([(header::CONTENT_TYPE, "application/dash+xml")], xml)
}
async fn send_mp4(AuthBasic((id, password)): AuthBasic, State(state): State<Arc<AppState>>) -> Response<Full<Bytes>> {
info!("segment request: auth {id:?}:{password:?}");
state.counter.fetch_add(1, Ordering::SeqCst);
let config = mp4::Mp4Config {
major_brand: str::parse("isom").unwrap(),
minor_version: 512,
compatible_brands: vec![
str::parse("isom").unwrap(),
str::parse("iso2").unwrap(),
str::parse("avc1").unwrap(),
str::parse("mp41").unwrap(),
],
timescale: 1000,
};
let data = Cursor::new(Vec::<u8>::new());
let mut writer = mp4::Mp4Writer::write_start(data, &config).unwrap();
let media_conf = mp4::MediaConfig::AvcConfig(mp4::AvcConfig {
width: 10,
height: 10,
seq_param_set: vec![
0, 0, 0, 0, ],
pic_param_set: vec![],
});
let track_conf = mp4::TrackConfig {
track_type: mp4::TrackType::Video,
timescale: 1000,
language: "und".to_string(),
media_conf,
};
writer.add_track(&track_conf).unwrap();
let sample = mp4::Mp4Sample {
start_time: 0,
duration: 2,
rendering_offset: 0,
is_sync: true,
bytes: mp4::Bytes::from(vec![0x0u8; 751]),
};
writer.write_sample(1, &sample).unwrap();
writer.write_end().unwrap();
let data: Vec<u8> = writer.into_writer().into_inner();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "video/mp4")
.body(Full::from(data))
.unwrap()
}
async fn send_status(State(state): State<Arc<AppState>>) -> impl IntoResponse {
([(header::CONTENT_TYPE, "text/plain")], format!("{}", state.counter.load(Ordering::Relaxed)))
}
env_logger::Builder::from_env(Env::default().default_filter_or("info,reqwest=warn")).init();
let app = Router::new()
.route("/mpd", get(send_mpd))
.route("/media/:seg", get(send_mp4))
.route("/status", get(send_status))
.with_state(shared_state);
let backend = async move {
axum::Server::bind(&"127.0.0.1:6666".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap()
};
tokio::spawn(backend);
tokio::time::sleep(Duration::from_millis(1000)).await;
let client = reqwest::Client::builder()
.timeout(Duration::new(10, 0))
.build()
.context("creating HTTP client")?;
let txt = client.get("http://localhost:6666/status")
.send().await?
.error_for_status()?
.text().await
.context("fetching status")?;
assert!(txt.eq("0"), "Expecting 0 fragment requests, saw {txt}");
let mpd_fail = client.get("http://localhost:6666/mpd")
.send().await
.expect("unauthenticated manifest request");
assert_eq!(mpd_fail.status(), StatusCode::BAD_REQUEST);
let segment_fail = client.get("http://localhost:6666/media/foo.mp4")
.send().await
.expect("unauthenticated segment request");
assert_eq!(segment_fail.status(), StatusCode::BAD_REQUEST);
let outpath = env::temp_dir().join("basic_auth.mp4");
let cli = Command::new("cargo")
.args(["run", "--no-default-features", "--",
"-v", "-v", "-v",
"--auth-username", "myuser", "--auth-password", "mypassword",
"-o", outpath.to_str().unwrap(),
"http://localhost:6666/mpd"])
.output()
.expect("failed spawning cargo run / dash-mpd-cli");
assert!(cli.status.success());
assert!(fs::metadata(outpath).is_ok());
let txt = client.get("http://localhost:6666/status")
.send().await?
.error_for_status()?
.text().await
.context("fetching status")?;
assert!(txt.eq("2"), "Expecting 2 fragment requests, saw {txt}");
let outpath = env::temp_dir().join("basic_auth2.mp4");
let cli = Command::new("cargo")
.args(["run", "--no-default-features", "--",
"--quiet",
"--auth-username", "myuser", "--auth-password", "mypassword",
"-o", outpath.to_str().unwrap(),
"http://localhost:6666/mpd"])
.output()
.expect("failed spawning cargo run / dash-mpd-cli");
assert!(cli.status.success());
assert!(fs::metadata(outpath).is_ok());
let txt = client.get("http://localhost:6666/status")
.send().await?
.error_for_status()?
.text().await
.context("fetching status")?;
assert!(txt.eq("3"), "Expecting 3 fragment requests, saw {txt}");
Ok(())
}