2024-02-14 21:48:58 +00:00
|
|
|
//! Using `wry`'s http module, we can stream a video file from the local file system.
|
|
|
|
//!
|
|
|
|
//! You could load in any file type, but this example uses a video file.
|
|
|
|
|
2024-01-20 00:36:40 +00:00
|
|
|
use dioxus::desktop::wry::http;
|
|
|
|
use dioxus::desktop::wry::http::Response;
|
|
|
|
use dioxus::desktop::{use_asset_handler, AssetRequest};
|
2023-12-18 14:32:50 +00:00
|
|
|
use dioxus::prelude::*;
|
|
|
|
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
|
2023-12-18 15:34:30 +00:00
|
|
|
use std::{io::SeekFrom, path::PathBuf};
|
2024-02-14 20:33:07 +00:00
|
|
|
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
|
2023-12-18 15:34:30 +00:00
|
|
|
|
|
|
|
const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
|
2023-12-18 14:32:50 +00:00
|
|
|
|
|
|
|
fn main() {
|
2024-02-14 21:48:58 +00:00
|
|
|
// For the sake of this example, we will download the video file if it doesn't exist
|
|
|
|
ensure_video_is_loaded();
|
|
|
|
|
2024-01-16 17:45:02 +00:00
|
|
|
launch_desktop(app);
|
2023-12-18 14:32:50 +00:00
|
|
|
}
|
|
|
|
|
2024-01-14 04:51:37 +00:00
|
|
|
fn app() -> Element {
|
2024-02-14 21:48:58 +00:00
|
|
|
// Any request to /videos will be handled by this handler
|
2024-01-14 04:51:37 +00:00
|
|
|
use_asset_handler("videos", move |request, responder| {
|
2024-01-06 01:35:37 +00:00
|
|
|
// Using dioxus::spawn works, but is slower than a dedicated thread
|
|
|
|
tokio::task::spawn(async move {
|
2023-12-18 15:34:30 +00:00
|
|
|
let video_file = PathBuf::from(VIDEO_PATH);
|
|
|
|
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
|
2024-01-06 01:35:37 +00:00
|
|
|
|
|
|
|
match get_stream_response(&mut file, &request).await {
|
|
|
|
Ok(response) => responder.respond(response),
|
|
|
|
Err(err) => eprintln!("Error: {}", err),
|
|
|
|
}
|
|
|
|
});
|
2023-12-18 14:32:50 +00:00
|
|
|
});
|
|
|
|
|
2024-01-16 19:18:46 +00:00
|
|
|
rsx! {
|
2024-01-06 01:35:37 +00:00
|
|
|
div {
|
|
|
|
video {
|
|
|
|
src: "/videos/test_video.mp4",
|
|
|
|
autoplay: true,
|
|
|
|
controls: true,
|
|
|
|
width: 640,
|
|
|
|
height: 480
|
|
|
|
}
|
|
|
|
}
|
2023-12-18 15:34:30 +00:00
|
|
|
}
|
2023-12-18 14:32:50 +00:00
|
|
|
}
|
|
|
|
|
2024-01-06 01:35:37 +00:00
|
|
|
/// This was taken from wry's example
|
2023-12-18 15:34:30 +00:00
|
|
|
async fn get_stream_response(
|
|
|
|
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
|
2023-12-18 14:32:50 +00:00
|
|
|
request: &AssetRequest,
|
|
|
|
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
2023-12-18 15:34:30 +00:00
|
|
|
// get stream length
|
2023-12-18 14:32:50 +00:00
|
|
|
let len = {
|
2023-12-18 15:34:30 +00:00
|
|
|
let old_pos = asset.stream_position().await?;
|
|
|
|
let len = asset.seek(SeekFrom::End(0)).await?;
|
|
|
|
asset.seek(SeekFrom::Start(old_pos)).await?;
|
2023-12-18 14:32:50 +00:00
|
|
|
len
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
|
|
|
|
|
|
|
|
// if the webview sent a range header, we need to send a 206 in return
|
|
|
|
// Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
|
|
|
|
let http_response = if let Some(range_header) = request.headers().get("range") {
|
|
|
|
let not_satisfiable = || {
|
|
|
|
ResponseBuilder::new()
|
|
|
|
.status(StatusCode::RANGE_NOT_SATISFIABLE)
|
|
|
|
.header(CONTENT_RANGE, format!("bytes */{len}"))
|
|
|
|
.body(vec![])
|
|
|
|
};
|
|
|
|
|
|
|
|
// parse range header
|
|
|
|
let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) {
|
|
|
|
ranges
|
|
|
|
.iter()
|
|
|
|
// map the output back to spec range <start-end>, example: 0-499
|
|
|
|
.map(|r| (r.start, r.start + r.length - 1))
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
} else {
|
|
|
|
return Ok(not_satisfiable()?);
|
|
|
|
};
|
|
|
|
|
|
|
|
/// The Maximum bytes we send in one range
|
|
|
|
const MAX_LEN: u64 = 1000 * 1024;
|
|
|
|
|
|
|
|
if ranges.len() == 1 {
|
|
|
|
let &(start, mut end) = ranges.first().unwrap();
|
|
|
|
|
|
|
|
// check if a range is not satisfiable
|
|
|
|
//
|
|
|
|
// this should be already taken care of by HttpRange::parse
|
|
|
|
// but checking here again for extra assurance
|
|
|
|
if start >= len || end >= len || end < start {
|
|
|
|
return Ok(not_satisfiable()?);
|
|
|
|
}
|
|
|
|
|
|
|
|
// adjust end byte for MAX_LEN
|
|
|
|
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
|
|
|
|
|
|
|
|
// calculate number of bytes needed to be read
|
|
|
|
let bytes_to_read = end + 1 - start;
|
|
|
|
|
|
|
|
// allocate a buf with a suitable capacity
|
|
|
|
let mut buf = Vec::with_capacity(bytes_to_read as usize);
|
|
|
|
// seek the file to the starting byte
|
2023-12-18 15:34:30 +00:00
|
|
|
asset.seek(SeekFrom::Start(start)).await?;
|
2023-12-18 14:32:50 +00:00
|
|
|
// read the needed bytes
|
2023-12-18 15:34:30 +00:00
|
|
|
asset.take(bytes_to_read).read_to_end(&mut buf).await?;
|
2023-12-18 14:32:50 +00:00
|
|
|
|
|
|
|
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
|
|
|
|
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
|
|
|
|
resp = resp.status(StatusCode::PARTIAL_CONTENT);
|
|
|
|
resp.body(buf)
|
|
|
|
} else {
|
|
|
|
let mut buf = Vec::new();
|
|
|
|
let ranges = ranges
|
|
|
|
.iter()
|
|
|
|
.filter_map(|&(start, mut end)| {
|
|
|
|
// filter out unsatisfiable ranges
|
|
|
|
//
|
|
|
|
// this should be already taken care of by HttpRange::parse
|
|
|
|
// but checking here again for extra assurance
|
|
|
|
if start >= len || end >= len || end < start {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
// adjust end byte for MAX_LEN
|
|
|
|
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
|
|
|
|
Some((start, end))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
2023-12-18 15:34:30 +00:00
|
|
|
let boundary = format!("{:x}", rand::random::<u64>());
|
2023-12-18 14:32:50 +00:00
|
|
|
let boundary_sep = format!("\r\n--{boundary}\r\n");
|
|
|
|
let boundary_closer = format!("\r\n--{boundary}\r\n");
|
|
|
|
|
|
|
|
resp = resp.header(
|
|
|
|
CONTENT_TYPE,
|
|
|
|
format!("multipart/byteranges; boundary={boundary}"),
|
|
|
|
);
|
|
|
|
|
|
|
|
for (end, start) in ranges {
|
|
|
|
// a new range is being written, write the range boundary
|
2023-12-18 15:34:30 +00:00
|
|
|
buf.write_all(boundary_sep.as_bytes()).await?;
|
2023-12-18 14:32:50 +00:00
|
|
|
|
|
|
|
// write the needed headers `Content-Type` and `Content-Range`
|
2023-12-18 15:34:30 +00:00
|
|
|
buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
|
|
|
|
.await?;
|
|
|
|
buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
|
|
|
|
.await?;
|
2023-12-18 14:32:50 +00:00
|
|
|
|
|
|
|
// write the separator to indicate the start of the range body
|
2023-12-18 15:34:30 +00:00
|
|
|
buf.write_all("\r\n".as_bytes()).await?;
|
2023-12-18 14:32:50 +00:00
|
|
|
|
|
|
|
// calculate number of bytes needed to be read
|
|
|
|
let bytes_to_read = end + 1 - start;
|
|
|
|
|
|
|
|
let mut local_buf = vec![0_u8; bytes_to_read as usize];
|
2023-12-18 15:34:30 +00:00
|
|
|
asset.seek(SeekFrom::Start(start)).await?;
|
|
|
|
asset.read_exact(&mut local_buf).await?;
|
2023-12-18 14:32:50 +00:00
|
|
|
buf.extend_from_slice(&local_buf);
|
|
|
|
}
|
|
|
|
// all ranges have been written, write the closing boundary
|
2023-12-18 15:34:30 +00:00
|
|
|
buf.write_all(boundary_closer.as_bytes()).await?;
|
2023-12-18 14:32:50 +00:00
|
|
|
|
|
|
|
resp.body(buf)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
resp = resp.header(CONTENT_LENGTH, len);
|
|
|
|
let mut buf = Vec::with_capacity(len as usize);
|
2023-12-18 15:34:30 +00:00
|
|
|
asset.read_to_end(&mut buf).await?;
|
2023-12-18 14:32:50 +00:00
|
|
|
resp.body(buf)
|
|
|
|
};
|
|
|
|
|
|
|
|
http_response.map_err(Into::into)
|
|
|
|
}
|
2024-02-14 21:48:58 +00:00
|
|
|
|
|
|
|
fn ensure_video_is_loaded() {
|
|
|
|
let video_file = PathBuf::from(VIDEO_PATH);
|
|
|
|
if !video_file.exists() {
|
|
|
|
tokio::runtime::Runtime::new()
|
|
|
|
.unwrap()
|
|
|
|
.block_on(async move {
|
|
|
|
println!("Downloading video file...");
|
|
|
|
let video_url =
|
|
|
|
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
|
|
|
|
let mut response = reqwest::get(video_url).await.unwrap();
|
|
|
|
let mut file = tokio::fs::File::create(&video_file).await.unwrap();
|
|
|
|
while let Some(chunk) = response.chunk().await.unwrap() {
|
|
|
|
file.write_all(&chunk).await.unwrap();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|