rust 61 lines · 8 steps

HTTP range requests for video streaming in Axum

An Axum handler serves partial video content so browsers can seek and stream without downloading the whole file.

Explained by highlit
1use axum::{
2 body::Body,
3 extract::Path,
4 http::{header, HeaderMap, HeaderValue, StatusCode},
5 response::{IntoResponse, Response},
6};
7use tokio::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
8use tokio_util::io::ReaderStream;
9 
10pub async fn stream_video(Path(name): Path<String>, headers: HeaderMap) -> Response {
11 let path = std::path::Path::new("media").join(&name);
12 let mut file = match tokio::fs::File::open(&path).await {
13 Ok(f) => f,
14 Err(_) => return StatusCode::NOT_FOUND.into_response(),
15 };
16 let total = match file.metadata().await {
17 Ok(m) => m.len(),
18 Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
19 };
20 
21 let range = headers.get(header::RANGE).and_then(|v| v.to_str().ok());
22 let (start, end) = match parse_range(range, total) {
23 Some(r) => r,
24 None if range.is_some() => {
25 return Response::builder()
26 .status(StatusCode::RANGE_NOT_SATISFIABLE)
27 .header(header::CONTENT_RANGE, format!("bytes */{total}"))
28 .body(Body::empty())
29 .unwrap();
30 }
31 None => (0, total - 1),
32 };
33 
34 let len = end - start + 1;
35 if file.seek(SeekFrom::Start(start)).await.is_err() {
36 return StatusCode::INTERNAL_SERVER_ERROR.into_response();
37 }
38 let stream = ReaderStream::new(file.take(len));
39 
40 let status = if range.is_some() { StatusCode::PARTIAL_CONTENT } else { StatusCode::OK };
41 let mut builder = Response::builder()
42 .status(status)
43 .header(header::CONTENT_TYPE, "video/mp4")
44 .header(header::ACCEPT_RANGES, "bytes")
45 .header(header::CONTENT_LENGTH, len);
46 if range.is_some() {
47 builder = builder.header(
48 header::CONTENT_RANGE,
49 HeaderValue::from_str(&format!("bytes {start}-{end}/{total}")).unwrap(),
50 );
51 }
52 builder.body(Body::from_stream(stream)).unwrap()
53}
54 
55fn parse_range(header: Option<&str>, total: u64) -> Option<(u64, u64)> {
56 let spec = header?.strip_prefix("bytes=")?;
57 let (s, e) = spec.split_once('-')?;
58 let start: u64 = if s.is_empty() { 0 } else { s.parse().ok()? };
59 let end: u64 = if e.is_empty() { total - 1 } else { e.parse().ok()?.min(total - 1) };
60 (start <= end && start < total).then_some((start, end))
61}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Range requests let clients ask for byte slices, which is what makes video seeking work over HTTP.
  2. 2Streaming a bounded reader avoids loading an entire file into memory before responding.
  3. 3A malformed range must answer 416 with a Content-Range header rather than silently serving the whole file.

Related explainers

Share this explainer

Here's the card — post it anywhere.

HTTP range requests for video streaming in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code