mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-03 10:11:15 +02:00
Co-authored-by: wusyong <wusyong@users.noreply.github.com> Co-authored-by: Fabian-Lars <fabianlars@fabianlars.de> Co-authored-by: Lucas Nogueira <lucas@tauri.studio> Co-authored-by: Simon Hyll <hyllsimon@gmail.com> Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app> Co-authored-by: Lucas Nogueira <lucas@tauri.app> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.studio> Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: chip <chip@chip.sh> Co-authored-by: Raphii <iam@raphii.co> Co-authored-by: Ronie Martinez <ronmarti18@gmail.com> Co-authored-by: hanaTsuk1 <101488209+hanaTsuk1@users.noreply.github.com> Co-authored-by: nathan-fall <39990940+nathan-fall@users.noreply.github.com> Co-authored-by: Akshay <nerdy@peppe.rs> Co-authored-by: KurikoMoe <kurikomoe@gmail.com> Co-authored-by: Guilherme Oenning <me@goenning.net> Co-authored-by: Pierre Cashon <biaocy91@gmail.com> Co-authored-by: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Co-authored-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com> Co-authored-by: Risto Stevcev <me@risto.codes> Co-authored-by: Soumt <rltks1305@naver.com> Co-authored-by: yutotnh <57719497+yutotnh@users.noreply.github.com> Co-authored-by: Gökçe Merdun <agmmnn@gmail.com> Co-authored-by: Nathanael Rea <Nathan@NathanaelRea.com> Co-authored-by: Usman Rajab <usman.rajab@gmail.com> Co-authored-by: Francis The Basilisk <36006338+snorkysnark@users.noreply.github.com> Co-authored-by: Lej77 <31554212+Lej77@users.noreply.github.com> Co-authored-by: Tomáš Diblík <dibla.tomas@post.cz> Co-authored-by: Jonas Kruckenberg <iterpre@protonmail.com> Co-authored-by: Pascal Sommer <Pascal-So@users.noreply.github.com> Co-authored-by: Bo <bertonzh@gmail.com> Co-authored-by: Kevin Yue <k3vinyue@gmail.com> fixed grammar and typos (#6937) Fix api.js docs pipeline with updated typedoc dependencies (#6945) closes #6887 (#6922) fix(core): Fix `WindowBuilder::on_navigation` handler never registerd, closes #6865 (#6921) fix(core): Fix `WindowBuilder::on_navigation` handler never registerd, closes #6865 fix broken symlinks in license files (#6336) fix(cli): fix cli connection timeout to dev server (fix #6045) (#6046) fix(bundler): ensure that there are no duplicate extension arguments when bundling on Windows, fixes #6103 (#6917) fix(bundler): ensure that there are no duplicate extension arguments during bundling on Windows (fix #6103) closes #5491 (#6408) fix(nsis): prefill $INSTDIR with previous install path and respect `/D` flag, closes #6928 (#6935) fix(nsis): prefill $INSTDIR with previous install path and respect `/D` flag, closes #6928 fix(updater): emit `UPTODATE` when server responds with 204, closes #6934 (#6970) fix(core): unpin all dependencies, closes #6944 (#6966) fix(bundler): Add new lang_file option in persian variant. (#6972) fix(core/ipc): access url through webview native object, closes #6889 (#6976) fix(core): remove trailing slash in http scope url, closes #5208 (#6974) fix(core): remove trailing slash in http scope url, closes #5208 fix(cli): find correct binary when `--profile` is used, closes #6954 (#6979) fix(cli): find correct binary when `--profile` is used, closes #6954 closes #6955 (#6987) closes #6955 closes #6158 (#6969) closes #6158 fix(cli): improve vs build tools detection (#6982) fix: updated appimage script to follow symlinks for /usr/lib* (fix: #6992) (#6996) fix(cli): correctly remove Cargo features (#7013) Fix typo (#7012) fix(cli): revert metadata.json field rename from #6795 (#7029) closes #6732 (#6736) fix: add missing file properties on Windows, closes #6676 (#6693) fix(cli.js): detect node-20 binary (#6667) fix version-or-publish workflow (#7031) fix(cli/devserver): inject autoreload into HTML only, closes #6997 (#7032) fix(bundler/nsis): write installer templates UTF16LE encoded, closes #7036 (#7040) fix(bundler/nsis): write installer templates UTF16LE encoded, closes #7036 fix(core): rewrite `asset` protocol streaming, closes #6375 (#6390) closes #5939 (#5960) fix(core): use `safe_block_on` (#7047) closes #6859 (#6933) closes #6955 (#6998) fix(core): populate webview_attrs from config, closes #6794 (#6797) closes #5176 (#5180) fix: sound for notifications on windows (fix #6652) (#6680) close native window's buttons, closes #2353 (#6665) fix(bundler/nsis): calculate accurate app size, closes #7056 (#7057) fix(tests): only download update when it is available (#7061) closes #6706 (#6712) fix(doc): correct the doc of `content_protected()` (#7065) closes #6472 (#6530) fix(macros): use full path to Result to avoid issues with type aliases (#7071)
203 lines
6.9 KiB
Rust
203 lines
6.9 KiB
Rust
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
fn main() {
|
|
use std::{
|
|
io::{Read, Seek, SeekFrom, Write},
|
|
path::PathBuf,
|
|
process::{Command, Stdio},
|
|
};
|
|
use tauri::http::{header::*, status::StatusCode, HttpRange, ResponseBuilder};
|
|
|
|
let video_file = PathBuf::from("test_video.mp4");
|
|
let video_url =
|
|
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
|
|
|
|
if !video_file.exists() {
|
|
// Downloading with curl this saves us from adding
|
|
// a Rust HTTP client dependency.
|
|
println!("Downloading {video_url}");
|
|
let status = Command::new("curl")
|
|
.arg("-L")
|
|
.arg("-o")
|
|
.arg(&video_file)
|
|
.arg(video_url)
|
|
.stdout(Stdio::inherit())
|
|
.stderr(Stdio::inherit())
|
|
.output()
|
|
.unwrap();
|
|
|
|
assert!(status.status.success());
|
|
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| {
|
|
// 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 != "test_video.mp4" {
|
|
// return error 404 if it's not our video
|
|
return ResponseBuilder::new().status(404).body(Vec::new());
|
|
}
|
|
|
|
let mut file = std::fs::File::open(&path)?;
|
|
|
|
// 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.
|
|
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();
|
|
}
|
|
|
|
// 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 {
|
|
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())?;
|
|
|
|
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"
|
|
))
|
|
.expect("error while running tauri application");
|
|
}
|
|
|
|
// returns the scheme and the path of the video file
|
|
// we're using this just to allow using the custom `stream` protocol or tauri built-in `asset` protocol
|
|
#[tauri::command]
|
|
fn video_uri() -> (&'static str, std::path::PathBuf) {
|
|
#[cfg(feature = "protocol-asset")]
|
|
{
|
|
let mut path = std::env::current_dir().unwrap();
|
|
path.push("test_video.mp4");
|
|
("asset", path)
|
|
}
|
|
|
|
#[cfg(not(feature = "protocol-asset"))]
|
|
("stream", "test_video.mp4".into())
|
|
}
|