|
@@ -4,14 +4,15 @@
|
|
|
|
|
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
|
|
+use std::sync::{Arc, Mutex};
|
|
|
+
|
|
|
fn main() {
|
|
|
use std::{
|
|
|
- cmp::min,
|
|
|
- io::{Read, Seek, SeekFrom},
|
|
|
+ io::{Read, Seek, SeekFrom, Write},
|
|
|
path::PathBuf,
|
|
|
process::{Command, Stdio},
|
|
|
};
|
|
|
- use tauri::http::{HttpRange, ResponseBuilder};
|
|
|
+ use tauri::http::{header::*, status::StatusCode, HttpRange, ResponseBuilder};
|
|
|
|
|
|
let video_file = PathBuf::from("test_video.mp4");
|
|
|
let video_url =
|
|
@@ -35,77 +36,149 @@ fn main() {
|
|
|
assert!(video_file.exists());
|
|
|
}
|
|
|
|
|
|
+ // NOTE: for production use `rand` crate to generate a random boundary
|
|
|
+ let boundary_id = Arc::new(Mutex::new(0));
|
|
|
+
|
|
|
tauri::Builder::default()
|
|
|
.invoke_handler(tauri::generate_handler![video_uri])
|
|
|
.register_uri_scheme_protocol("stream", move |_app, request| {
|
|
|
- // prepare our response
|
|
|
- let mut response = ResponseBuilder::new();
|
|
|
// get the file path
|
|
|
let path = request.uri().strip_prefix("stream://localhost/").unwrap();
|
|
|
let path = percent_encoding::percent_decode(path.as_bytes())
|
|
|
.decode_utf8_lossy()
|
|
|
.to_string();
|
|
|
|
|
|
- if path != "example/test_video.mp4" {
|
|
|
- // return error 404 if it's not out video
|
|
|
- return response.mimetype("text/plain").status(404).body(Vec::new());
|
|
|
+ if path != "test_video.mp4" {
|
|
|
+ // return error 404 if it's not our video
|
|
|
+ return ResponseBuilder::new().status(404).body(Vec::new());
|
|
|
}
|
|
|
|
|
|
- // read our file
|
|
|
- let mut content = std::fs::File::open(&video_file)?;
|
|
|
- let mut buf = Vec::new();
|
|
|
+ let mut file = std::fs::File::open(&path)?;
|
|
|
|
|
|
- // default status code
|
|
|
- let mut status_code = 200;
|
|
|
+ // get file length
|
|
|
+ let len = {
|
|
|
+ let old_pos = file.stream_position()?;
|
|
|
+ let len = file.seek(SeekFrom::End(0))?;
|
|
|
+ file.seek(SeekFrom::Start(old_pos))?;
|
|
|
+ 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.
|
|
|
- if let Some(range) = request.headers().get("range") {
|
|
|
- // Get the file size
|
|
|
- let file_size = content.metadata().unwrap().len();
|
|
|
-
|
|
|
- // we parse the range header with tauri helper
|
|
|
- let range = HttpRange::parse(range.to_str().unwrap(), file_size).unwrap();
|
|
|
- // let support only 1 range for now
|
|
|
- let first_range = range.first();
|
|
|
- if let Some(range) = first_range {
|
|
|
- let mut real_length = range.length;
|
|
|
-
|
|
|
- // prevent max_length;
|
|
|
- // specially on webview2
|
|
|
- if range.length > file_size / 3 {
|
|
|
- // max size sent (400ko / request)
|
|
|
- // as it's local file system we can afford to read more often
|
|
|
- real_length = min(file_size - range.start, 1024 * 400);
|
|
|
+ let 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) = 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 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 not_satisfiable();
|
|
|
}
|
|
|
|
|
|
- // last byte we are reading, the length of the range include the last byte
|
|
|
- // who should be skipped on the header
|
|
|
- let last_byte = range.start + real_length - 1;
|
|
|
- // partial content
|
|
|
- status_code = 206;
|
|
|
-
|
|
|
- // Only macOS and Windows are supported, if you set headers in linux they are ignored
|
|
|
- response = response
|
|
|
- .header("Connection", "Keep-Alive")
|
|
|
- .header("Accept-Ranges", "bytes")
|
|
|
- .header("Content-Length", real_length)
|
|
|
- .header(
|
|
|
- "Content-Range",
|
|
|
- format!("bytes {}-{}/{}", range.start, last_byte, file_size),
|
|
|
- );
|
|
|
-
|
|
|
- // FIXME: Add ETag support (caching on the webview)
|
|
|
-
|
|
|
- // seek our file bytes
|
|
|
- content.seek(SeekFrom::Start(range.start))?;
|
|
|
- content.take(real_length).read_to_end(&mut buf)?;
|
|
|
+ // 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
|
|
|
+ file.seek(SeekFrom::Start(start))?;
|
|
|
+ // read the needed bytes
|
|
|
+ file.take(bytes_to_read).read_to_end(&mut buf)?;
|
|
|
+
|
|
|
+ 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 {
|
|
|
- content.read_to_end(&mut buf)?;
|
|
|
- }
|
|
|
- }
|
|
|
+ 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<_>>();
|
|
|
+
|
|
|
+ let mut id = boundary_id.lock().unwrap();
|
|
|
+ *id += 1;
|
|
|
+ let boundary = format!("sadasq2e{id}");
|
|
|
+ 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
|
|
|
+ buf.write_all(boundary_sep.as_bytes())?;
|
|
|
+
|
|
|
+ // 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_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())?;
|
|
|
+
|
|
|
+ // write the separator to indicate the start of the range body
|
|
|
+ buf.write_all("\r\n".as_bytes())?;
|
|
|
+
|
|
|
+ // 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];
|
|
|
+ file.seek(SeekFrom::Start(start))?;
|
|
|
+ file.read_exact(&mut local_buf)?;
|
|
|
+ buf.extend_from_slice(&local_buf);
|
|
|
+ }
|
|
|
+ // all ranges have been written, write the closing boundary
|
|
|
+ buf.write_all(boundary_closer.as_bytes())?;
|
|
|
|
|
|
- response.mimetype("video/mp4").status(status_code).body(buf)
|
|
|
+ resp.body(buf)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ resp = resp.header(CONTENT_LENGTH, len);
|
|
|
+ let mut buf = Vec::with_capacity(len as usize);
|
|
|
+ file.read_to_end(&mut buf)?;
|
|
|
+ resp.body(buf)
|
|
|
+ };
|
|
|
+
|
|
|
+ response
|
|
|
})
|
|
|
.run(tauri::generate_context!(
|
|
|
"../../examples/streaming/tauri.conf.json"
|
|
@@ -125,5 +198,5 @@ fn video_uri() -> (&'static str, std::path::PathBuf) {
|
|
|
}
|
|
|
|
|
|
#[cfg(not(feature = "protocol-asset"))]
|
|
|
- ("stream", "example/test_video.mp4".into())
|
|
|
+ ("stream", "test_video.mp4".into())
|
|
|
}
|