make the video streaming async

This commit is contained in:
Evan Almloff committed 2023-12-18 09:34:30 -06:00
1 parent ec4c1e0a44
commit dfb118e06c
3 files changed
+65 -60

No files matched your search

+1
View File
@@ -4,6 +4,7 @@
/dist /dist
Cargo.lock Cargo.lock
.DS_Store .DS_Store
/examples/assets/test_video.mp4
.vscode/* .vscode/*
!.vscode/settings.json !.vscode/settings.json
+57 -51
View File
@@ -4,56 +4,64 @@ use dioxus_desktop::wry::http::Response;
use dioxus_desktop::{use_asset_handler, AssetRequest}; use dioxus_desktop::{use_asset_handler, AssetRequest};
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode}; use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
use std::borrow::Cow; use std::borrow::Cow;
use std::sync::{Arc, Mutex}; use std::{io::SeekFrom, path::PathBuf};
use std::{ use tokio::io::AsyncReadExt;
io::{Read, Seek, SeekFrom, Write}, use tokio::io::AsyncSeekExt;
path::PathBuf, use tokio::io::AsyncWriteExt;
};
const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
fn main() { fn main() {
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();
}
});
}
dioxus_desktop::launch(app); dioxus_desktop::launch(app);
} }
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {
let id = Arc::new(Mutex::new(0));
use_asset_handler(cx, move |request: &AssetRequest| { use_asset_handler(cx, move |request: &AssetRequest| {
let response: Option<Response<Cow<'static, [u8]>>> = match get_stream_response(request, &id) let request = request.clone();
{ async move {
Ok(response) => Some(response.map(Cow::Owned)), let video_file = PathBuf::from(VIDEO_PATH);
Err(err) => { let mut file = tokio::fs::File::open(&video_file).await.unwrap();
eprintln!("Error: {}", err); let response: Option<Response<Cow<'static, [u8]>>> =
None match get_stream_response(&mut file, &request).await {
} Ok(response) => Some(response.map(Cow::Owned)),
}; Err(err) => {
async move { response } eprintln!("Error: {}", err);
None
}
};
response
}
}); });
cx.render(rsx! { render! {
div { div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } }
video { }
src: "test_video.mp4",
autoplay: true,
controls: true,
width: 640,
height: 480,
}
}
})
} }
fn get_stream_response( async fn get_stream_response(
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
request: &AssetRequest, request: &AssetRequest,
boundary_id: &Arc<Mutex<i32>>,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> { ) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
let video_file = PathBuf::from("./examples/assets/test_video.mp4"); // get stream length
let mut file = std::fs::File::open(&video_file)?;
// get file length
let len = { let len = {
let old_pos = file.stream_position()?; let old_pos = asset.stream_position().await?;
let len = file.seek(SeekFrom::End(0))?; let len = asset.seek(SeekFrom::End(0)).await?;
file.seek(SeekFrom::Start(old_pos))?; asset.seek(SeekFrom::Start(old_pos)).await?;
len len
}; };
@@ -103,9 +111,9 @@ fn get_stream_response(
// allocate a buf with a suitable capacity // allocate a buf with a suitable capacity
let mut buf = Vec::with_capacity(bytes_to_read as usize); let mut buf = Vec::with_capacity(bytes_to_read as usize);
// seek the file to the starting byte // seek the file to the starting byte
file.seek(SeekFrom::Start(start))?; asset.seek(SeekFrom::Start(start)).await?;
// read the needed bytes // read the needed bytes
file.take(bytes_to_read).read_to_end(&mut buf)?; asset.take(bytes_to_read).read_to_end(&mut buf).await?;
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
resp = resp.header(CONTENT_LENGTH, end + 1 - start); resp = resp.header(CONTENT_LENGTH, end + 1 - start);
@@ -130,9 +138,7 @@ fn get_stream_response(
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut id = boundary_id.lock().unwrap(); let boundary = format!("{:x}", rand::random::<u64>());
*id += 1;
let boundary = format!("sadasq2e{id}");
let boundary_sep = format!("\r\n--{boundary}\r\n"); let boundary_sep = format!("\r\n--{boundary}\r\n");
let boundary_closer = format!("\r\n--{boundary}\r\n"); let boundary_closer = format!("\r\n--{boundary}\r\n");
@@ -143,34 +149,34 @@ fn get_stream_response(
for (end, start) in ranges { for (end, start) in ranges {
// a new range is being written, write the range boundary // a new range is being written, write the range boundary
buf.write_all(boundary_sep.as_bytes())?; buf.write_all(boundary_sep.as_bytes()).await?;
// write the needed headers `Content-Type` and `Content-Range` // write the needed headers `Content-Type` and `Content-Range`
buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())?; buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
buf.write_all( .await?;
format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes(), buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
)?; .await?;
// write the separator to indicate the start of the range body // write the separator to indicate the start of the range body
buf.write_all("\r\n".as_bytes())?; buf.write_all("\r\n".as_bytes()).await?;
// calculate number of bytes needed to be read // calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start; let bytes_to_read = end + 1 - start;
let mut local_buf = vec![0_u8; bytes_to_read as usize]; let mut local_buf = vec![0_u8; bytes_to_read as usize];
file.seek(SeekFrom::Start(start))?; asset.seek(SeekFrom::Start(start)).await?;
file.read_exact(&mut local_buf)?; asset.read_exact(&mut local_buf).await?;
buf.extend_from_slice(&local_buf); buf.extend_from_slice(&local_buf);
} }
// all ranges have been written, write the closing boundary // all ranges have been written, write the closing boundary
buf.write_all(boundary_closer.as_bytes())?; buf.write_all(boundary_closer.as_bytes()).await?;
resp.body(buf) resp.body(buf)
} }
} else { } else {
resp = resp.header(CONTENT_LENGTH, len); resp = resp.header(CONTENT_LENGTH, len);
let mut buf = Vec::with_capacity(len as usize); let mut buf = Vec::with_capacity(len as usize);
file.read_to_end(&mut buf)?; asset.read_to_end(&mut buf).await?;
resp.body(buf) resp.body(buf)
}; };
+7 -9
View File
@@ -4,7 +4,7 @@ use slab::Slab;
use std::{ use std::{
borrow::Cow, borrow::Cow,
future::Future, future::Future,
ops::{Deref, DerefMut}, ops::Deref,
path::{Path, PathBuf}, path::{Path, PathBuf},
pin::Pin, pin::Pin,
rc::Rc, rc::Rc,
@@ -72,10 +72,11 @@ pub type AssetResponse = Response<Cow<'static, [u8]>>;
pub trait AssetFuture: Future<Output = Option<AssetResponse>> + Send + Sync + 'static {} pub trait AssetFuture: Future<Output = Option<AssetResponse>> + Send + Sync + 'static {}
impl<T: Future<Output = Option<AssetResponse>> + Send + Sync + 'static> AssetFuture for T {} impl<T: Future<Output = Option<AssetResponse>> + Send + Sync + 'static> AssetFuture for T {}
#[derive(Debug, Clone)]
/// A request for an asset. This is a wrapper around [`Request<Vec<u8>>`] that provides methods specific to asset requests. /// A request for an asset. This is a wrapper around [`Request<Vec<u8>>`] that provides methods specific to asset requests.
pub struct AssetRequest { pub struct AssetRequest {
path: PathBuf, path: PathBuf,
request: Request<Vec<u8>>, request: Arc<Request<Vec<u8>>>,
} }
impl AssetRequest { impl AssetRequest {
@@ -90,7 +91,10 @@ impl From<Request<Vec<u8>>> for AssetRequest {
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/')) let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
.expect("expected URL to be UTF-8 encoded"); .expect("expected URL to be UTF-8 encoded");
let path = PathBuf::from(&*decoded); let path = PathBuf::from(&*decoded);
Self { request, path } Self {
request: Arc::new(request),
path,
}
} }
} }
@@ -102,12 +106,6 @@ impl Deref for AssetRequest {
} }
} }
impl DerefMut for AssetRequest {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.request
}
}
/// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`. /// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`.
/// This handler is stashed indefinitely in a context object, so it must be `'static`. /// This handler is stashed indefinitely in a context object, so it must be `'static`.
pub trait AssetHandler<F: AssetFuture>: Send + Sync + 'static { pub trait AssetHandler<F: AssetFuture>: Send + Sync + 'static {