Skip to content

Commit 9b1a6a1

Browse files
authored
fix(core): set correct mimetype for asset protocol streams, #5203 (#5536)
1 parent c6321a6 commit 9b1a6a1

File tree

6 files changed

+142
-47
lines changed

6 files changed

+142
-47
lines changed

Diff for: .changes/asset-protocol-streaming-mime-type.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": "patch"
3+
---
4+
5+
Set the correct mimetype when streaming files through `asset:` protocol

Diff for: core/tauri/src/manager.rs

+87-22
Original file line numberDiff line numberDiff line change
@@ -541,23 +541,39 @@ impl<R: Runtime> WindowManager<R> {
541541
.get("range")
542542
.and_then(|r| r.to_str().map(|r| r.to_string()).ok())
543543
{
544-
let (headers, status_code, data) = crate::async_runtime::safe_block_on(async move {
545-
let mut headers = HashMap::new();
546-
let mut buf = Vec::new();
544+
#[derive(Default)]
545+
struct RangeMetadata {
546+
file: Option<tokio::fs::File>,
547+
range: Option<crate::runtime::http::HttpRange>,
548+
metadata: Option<std::fs::Metadata>,
549+
headers: HashMap<&'static str, String>,
550+
status_code: u16,
551+
body: Vec<u8>,
552+
}
553+
554+
let mut range_metadata = crate::async_runtime::safe_block_on(async move {
555+
let mut data = RangeMetadata::default();
547556
// open the file
548557
let mut file = match tokio::fs::File::open(path_.clone()).await {
549558
Ok(file) => file,
550559
Err(e) => {
551560
debug_eprintln!("Failed to open asset: {}", e);
552-
return (headers, 404, buf);
561+
data.status_code = 404;
562+
return data;
553563
}
554564
};
555565
// Get the file size
556566
let file_size = match file.metadata().await {
557-
Ok(metadata) => metadata.len(),
567+
Ok(metadata) => {
568+
let len = metadata.len();
569+
data.metadata.replace(metadata);
570+
len
571+
}
558572
Err(e) => {
559573
debug_eprintln!("Failed to read asset metadata: {}", e);
560-
return (headers, 404, buf);
574+
data.file.replace(file);
575+
data.status_code = 404;
576+
return data;
561577
}
562578
};
563579
// parse the range
@@ -572,13 +588,16 @@ impl<R: Runtime> WindowManager<R> {
572588
Ok(r) => r,
573589
Err(e) => {
574590
debug_eprintln!("Failed to parse range {}: {:?}", range, e);
575-
return (headers, 400, buf);
591+
data.file.replace(file);
592+
data.status_code = 400;
593+
return data;
576594
}
577595
};
578596

579597
// FIXME: Support multiple ranges
580598
// let support only 1 range for now
581-
let status_code = if let Some(range) = range.first() {
599+
if let Some(range) = range.first() {
600+
data.range.replace(*range);
582601
let mut real_length = range.length;
583602
// prevent max_length;
584603
// specially on webview2
@@ -592,38 +611,84 @@ impl<R: Runtime> WindowManager<R> {
592611
// who should be skipped on the header
593612
let last_byte = range.start + real_length - 1;
594613

595-
headers.insert("Connection", "Keep-Alive".into());
596-
headers.insert("Accept-Ranges", "bytes".into());
597-
headers.insert("Content-Length", real_length.to_string());
598-
headers.insert(
614+
data.headers.insert("Connection", "Keep-Alive".into());
615+
data.headers.insert("Accept-Ranges", "bytes".into());
616+
data
617+
.headers
618+
.insert("Content-Length", real_length.to_string());
619+
data.headers.insert(
599620
"Content-Range",
600621
format!("bytes {}-{}/{}", range.start, last_byte, file_size),
601622
);
602623

603624
if let Err(e) = file.seek(std::io::SeekFrom::Start(range.start)).await {
604625
debug_eprintln!("Failed to seek file to {}: {}", range.start, e);
605-
return (headers, 422, buf);
626+
data.file.replace(file);
627+
data.status_code = 422;
628+
return data;
606629
}
607630

608-
if let Err(e) = file.take(real_length).read_to_end(&mut buf).await {
631+
let mut f = file.take(real_length);
632+
let r = f.read_to_end(&mut data.body).await;
633+
file = f.into_inner();
634+
data.file.replace(file);
635+
636+
if let Err(e) = r {
609637
debug_eprintln!("Failed read file: {}", e);
610-
return (headers, 422, buf);
638+
data.status_code = 422;
639+
return data;
611640
}
612641
// partial content
613-
206
642+
data.status_code = 206;
614643
} else {
615-
200
616-
};
644+
data.status_code = 200;
645+
}
617646

618-
(headers, status_code, buf)
647+
data
619648
});
620649

621-
for (k, v) in headers {
650+
for (k, v) in range_metadata.headers {
622651
response = response.header(k, v);
623652
}
624653

625-
let mime_type = MimeType::parse(&data, &path);
626-
response.mimetype(&mime_type).status(status_code).body(data)
654+
let mime_type = if let (Some(mut file), Some(metadata), Some(range)) = (
655+
range_metadata.file,
656+
range_metadata.metadata,
657+
range_metadata.range,
658+
) {
659+
// if we're already reading the beginning of the file, we do not need to re-read it
660+
if range.start == 0 {
661+
MimeType::parse(&range_metadata.body, &path)
662+
} else {
663+
let (status, bytes) = crate::async_runtime::safe_block_on(async move {
664+
let mut status = None;
665+
if let Err(e) = file.rewind().await {
666+
debug_eprintln!("Failed to rewind file: {}", e);
667+
status.replace(422);
668+
(status, Vec::with_capacity(0))
669+
} else {
670+
// taken from https://docs.rs/infer/0.9.0/src/infer/lib.rs.html#240-251
671+
let limit = std::cmp::min(metadata.len(), 8192) as usize + 1;
672+
let mut bytes = Vec::with_capacity(limit);
673+
if let Err(e) = file.take(8192).read_to_end(&mut bytes).await {
674+
debug_eprintln!("Failed read file: {}", e);
675+
status.replace(422);
676+
}
677+
(status, bytes)
678+
}
679+
});
680+
if let Some(s) = status {
681+
range_metadata.status_code = s;
682+
}
683+
MimeType::parse(&bytes, &path)
684+
}
685+
} else {
686+
MimeType::parse(&range_metadata.body, &path)
687+
};
688+
response
689+
.mimetype(&mime_type)
690+
.status(range_metadata.status_code)
691+
.body(range_metadata.body)
627692
} else {
628693
match crate::async_runtime::safe_block_on(async move { tokio::fs::read(path_).await }) {
629694
Ok(data) => {

Diff for: examples/streaming/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
A simple Tauri Application showcase the streaming functionality.
44

55
To execute run the following on the root directory of the repository: `cargo run --example streaming`.
6+
7+
By default the example uses a custom URI scheme protocol. To use the builtin `asset` protocol, run `cargo run --example streaming --features protocol-asset`.

Diff for: examples/streaming/index.html

+26-22
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
<!DOCTYPE html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<style>
7-
video {
8-
width: 100vw;
9-
height: 100vh;
10-
}
11-
</style>
12-
</head>
133

14-
<body>
15-
<video id="video_source" controls="" autoplay="" name="media">
16-
<source type="video/mp4" />
17-
</video>
18-
<script>
19-
const { convertFileSrc } = window.__TAURI__.tauri
20-
const video = document.getElementById('video_source')
21-
const source = document.createElement('source')
4+
<head>
5+
<meta charset="UTF-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<style>
8+
video {
9+
width: 100vw;
10+
height: 100vh;
11+
}
12+
</style>
13+
</head>
14+
15+
<body>
16+
<video id="video_source" controls="" autoplay="" name="media">
17+
<source type="video/mp4" />
18+
</video>
19+
<script>
20+
const { invoke, convertFileSrc } = window.__TAURI__.tauri
21+
const video = document.getElementById('video_source')
22+
const source = document.createElement('source')
23+
invoke('video_uri').then(([scheme, path]) => {
2224
source.type = 'video/mp4'
23-
source.src = convertFileSrc('example/test_video.mp4', 'stream')
25+
source.src = convertFileSrc(path, scheme)
2426
video.appendChild(source)
2527
video.load()
26-
</script>
27-
</body>
28-
</html>
28+
})
29+
</script>
30+
</body>
31+
32+
</html>

Diff for: examples/streaming/main.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,15 @@ fn main() {
3939
}
4040

4141
tauri::Builder::default()
42+
.invoke_handler(tauri::generate_handler![video_uri])
4243
.register_uri_scheme_protocol("stream", move |_app, request| {
4344
// prepare our response
4445
let mut response = ResponseBuilder::new();
4546
// get the wanted path
4647
#[cfg(target_os = "windows")]
4748
let path = request.uri().strip_prefix("stream://localhost/").unwrap();
4849
#[cfg(not(target_os = "windows"))]
49-
let path = request.uri().strip_prefix("stream://").unwrap();
50+
let path = request.uri().strip_prefix("stream://localhost/").unwrap();
5051
let path = percent_encoding::percent_decode(path.as_bytes())
5152
.decode_utf8_lossy()
5253
.to_string();
@@ -117,3 +118,18 @@ fn main() {
117118
))
118119
.expect("error while running tauri application");
119120
}
121+
122+
// returns the scheme and the path of the video file
123+
// we're using this just to allow using the custom `stream` protocol or tauri built-in `asset` protocol
124+
#[tauri::command]
125+
fn video_uri() -> (&'static str, std::path::PathBuf) {
126+
#[cfg(feature = "protocol-asset")]
127+
{
128+
let mut path = std::env::current_dir().unwrap();
129+
path.push("test_video.mp4");
130+
("asset", path)
131+
}
132+
133+
#[cfg(not(feature = "protocol-asset"))]
134+
("stream", "example/test_video.mp4".into())
135+
}

Diff for: examples/streaming/tauri.conf.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
}
3939
},
4040
"allowlist": {
41-
"all": false
41+
"all": false,
42+
"protocol": {
43+
"assetScope": ["**/test_video.mp4"]
44+
}
4245
},
4346
"windows": [
4447
{
@@ -50,7 +53,7 @@
5053
}
5154
],
5255
"security": {
53-
"csp": "default-src 'self'; media-src stream: https://stream.localhost"
56+
"csp": "default-src 'self'; media-src stream: https://stream.localhost asset: https://asset.localhost"
5457
},
5558
"updater": {
5659
"active": false

0 commit comments

Comments
 (0)