app
Adom Video Post-Production
Public Made by Adomby adom
AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones, speed up dead air, narrate, mux and auto-level, validate the final cut, and publish straight to the wiki, driven end to end by your AI.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
//! HTTP byte-range file streaming for `<video>` elements.
//!
//! Python's stock `http.server` returns `HTTP/1.0 200` with the full
//! body for any request, which makes Chromium's `<video>` stall on
//! seek because it's waiting for a `206 Partial Content` response to
//! a `Range: bytes=a-b` request that never comes. We hit this bug
//! live in the session that produced this module — the voiceover
//! subcommand has always done the right thing because it used this
//! code; the storyboard workflow didn't, and we had to write a
//! throwaway Python range-capable server to work around it before
//! this module existed.
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use tiny_http::{Header, Request, Response, StatusCode};
/// Stream a file in response to a request, respecting any
/// `Range: bytes=a-b` header. Always sends `Accept-Ranges: bytes`
/// so browsers know seeking is supported. Returns 206 Partial
/// Content if the request had a range header, 200 otherwise.
pub fn serve_file_range(request: Request, path: &Path, content_type: &str) -> Result<(), String> {
let mut file = File::open(path).map_err(|e| format!("open {}: {}", path.display(), e))?;
let total_len = file.metadata().map_err(|e| e.to_string())?.len();
let range = request
.headers()
.iter()
.find(|h| h.field.as_str().as_str().eq_ignore_ascii_case("Range"))
.and_then(|h| parse_range(h.value.as_str(), total_len));
let (start, end) = range.unwrap_or((0, total_len.saturating_sub(1)));
let length = end - start + 1;
file.seek(SeekFrom::Start(start)).map_err(|e| e.to_string())?;
let body = ReadLimit {
inner: file,
remaining: length,
};
let mut resp = Response::new(
StatusCode(if range.is_some() { 206 } else { 200 }),
vec![
Header::from_bytes(&b"Content-Type"[..], content_type.as_bytes()).unwrap(),
Header::from_bytes(&b"Accept-Ranges"[..], &b"bytes"[..]).unwrap(),
Header::from_bytes(&b"Cache-Control"[..], &b"no-cache"[..]).unwrap(),
],
body,
Some(length as usize),
None,
);
if range.is_some() {
let cr = format!("bytes {}-{}/{}", start, end, total_len);
resp.add_header(Header::from_bytes(&b"Content-Range"[..], cr.as_bytes()).unwrap());
}
request.respond(resp).map_err(|e| e.to_string())
}
/// Wrap a `Read` source so at most `remaining` bytes will ever be
/// yielded. Used to cap tiny_http's streaming body at the end of
/// the requested range.
struct ReadLimit<R: Read> {
inner: R,
remaining: u64,
}
impl<R: Read> Read for ReadLimit<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.remaining == 0 {
return Ok(0);
}
let max = std::cmp::min(buf.len() as u64, self.remaining) as usize;
let n = self.inner.read(&mut buf[..max])?;
self.remaining -= n as u64;
Ok(n)
}
}
/// Parse an HTTP `Range: bytes=a-b` header into inclusive `(start, end)`
/// offsets. Returns `None` for malformed or out-of-range values —
/// the caller then falls back to serving the whole file.
fn parse_range(header: &str, total: u64) -> Option<(u64, u64)> {
let s = header.strip_prefix("bytes=")?;
let mut parts = s.split('-');
let start = parts.next()?.parse::<u64>().ok()?;
let end = parts
.next()
.and_then(|p| if p.is_empty() { None } else { p.parse::<u64>().ok() })
.unwrap_or(total.saturating_sub(1));
if start > end || end >= total {
return None;
}
Some((start, end))
}