main.rs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
  5. use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
  6. use http_range::HttpRange;
  7. use std::{
  8. io::{Read, Seek, SeekFrom, Write},
  9. path::PathBuf,
  10. process::{Command, Stdio},
  11. };
  12. fn main() {
  13. let video_file = PathBuf::from("test_video.mp4");
  14. let video_url =
  15. "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
  16. if !video_file.exists() {
  17. // Downloading with curl this saves us from adding
  18. // a Rust HTTP client dependency.
  19. println!("Downloading {video_url}");
  20. let status = Command::new("curl")
  21. .arg("-L")
  22. .arg("-o")
  23. .arg(&video_file)
  24. .arg(video_url)
  25. .stdout(Stdio::inherit())
  26. .stderr(Stdio::inherit())
  27. .output()
  28. .unwrap();
  29. assert!(status.status.success());
  30. assert!(video_file.exists());
  31. }
  32. tauri::Builder::default()
  33. .invoke_handler(tauri::generate_handler![video_uri])
  34. .register_asynchronous_uri_scheme_protocol("stream", move |_app, request, responder| {
  35. match get_stream_response(request) {
  36. Ok(http_response) => responder.respond(http_response),
  37. Err(e) => responder.respond(
  38. ResponseBuilder::new()
  39. .status(StatusCode::BAD_REQUEST)
  40. .header(CONTENT_TYPE, "text/plain")
  41. .body(e.to_string().as_bytes().to_vec())
  42. .unwrap(),
  43. ),
  44. }
  45. })
  46. .run(tauri::generate_context!(
  47. "../../examples/streaming/tauri.conf.json"
  48. ))
  49. .expect("error while running tauri application");
  50. }
  51. // returns the scheme and the path of the video file
  52. // we're using this just to allow using the custom `stream` protocol or tauri built-in `asset` protocol
  53. #[tauri::command]
  54. fn video_uri() -> (&'static str, std::path::PathBuf) {
  55. #[cfg(feature = "protocol-asset")]
  56. {
  57. let mut path = std::env::current_dir().unwrap();
  58. path.push("test_video.mp4");
  59. ("asset", path)
  60. }
  61. #[cfg(not(feature = "protocol-asset"))]
  62. ("stream", "test_video.mp4".into())
  63. }
  64. fn get_stream_response(
  65. request: http::Request<Vec<u8>>,
  66. ) -> Result<http::Response<Vec<u8>>, Box<dyn std::error::Error>> {
  67. // skip leading `/`
  68. let path = percent_encoding::percent_decode(request.uri().path()[1..].as_bytes())
  69. .decode_utf8_lossy()
  70. .to_string();
  71. if path != "test_video.mp4" {
  72. // return error 404 if it's not our video
  73. return Ok(ResponseBuilder::new().status(404).body(Vec::new())?);
  74. }
  75. let mut file = std::fs::File::open(&path)?;
  76. // get file length
  77. let len = {
  78. let old_pos = file.stream_position()?;
  79. let len = file.seek(SeekFrom::End(0))?;
  80. file.seek(SeekFrom::Start(old_pos))?;
  81. len
  82. };
  83. let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
  84. // if the webview sent a range header, we need to send a 206 in return
  85. // Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
  86. let http_response = if let Some(range_header) = request.headers().get("range") {
  87. let not_satisfiable = || {
  88. ResponseBuilder::new()
  89. .status(StatusCode::RANGE_NOT_SATISFIABLE)
  90. .header(CONTENT_RANGE, format!("bytes */{len}"))
  91. .body(vec![])
  92. };
  93. // parse range header
  94. let ranges = if let Ok(ranges) = HttpRange::parse(range_header.to_str()?, len) {
  95. ranges
  96. .iter()
  97. // map the output back to spec range <start-end>, example: 0-499
  98. .map(|r| (r.start, r.start + r.length - 1))
  99. .collect::<Vec<_>>()
  100. } else {
  101. return Ok(not_satisfiable()?);
  102. };
  103. /// The Maximum bytes we send in one range
  104. const MAX_LEN: u64 = 1000 * 1024;
  105. if ranges.len() == 1 {
  106. let &(start, mut end) = ranges.first().unwrap();
  107. // check if a range is not satisfiable
  108. //
  109. // this should be already taken care of by HttpRange::parse
  110. // but checking here again for extra assurance
  111. if start >= len || end >= len || end < start {
  112. return Ok(not_satisfiable()?);
  113. }
  114. // adjust end byte for MAX_LEN
  115. end = start + (end - start).min(len - start).min(MAX_LEN - 1);
  116. // calculate number of bytes needed to be read
  117. let bytes_to_read = end + 1 - start;
  118. // allocate a buf with a suitable capacity
  119. let mut buf = Vec::with_capacity(bytes_to_read as usize);
  120. // seek the file to the starting byte
  121. file.seek(SeekFrom::Start(start))?;
  122. // read the needed bytes
  123. file.take(bytes_to_read).read_to_end(&mut buf)?;
  124. resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
  125. resp = resp.header(CONTENT_LENGTH, end + 1 - start);
  126. resp = resp.status(StatusCode::PARTIAL_CONTENT);
  127. resp.body(buf)
  128. } else {
  129. let mut buf = Vec::new();
  130. let ranges = ranges
  131. .iter()
  132. .filter_map(|&(start, mut end)| {
  133. // filter out unsatisfiable ranges
  134. //
  135. // this should be already taken care of by HttpRange::parse
  136. // but checking here again for extra assurance
  137. if start >= len || end >= len || end < start {
  138. None
  139. } else {
  140. // adjust end byte for MAX_LEN
  141. end = start + (end - start).min(len - start).min(MAX_LEN - 1);
  142. Some((start, end))
  143. }
  144. })
  145. .collect::<Vec<_>>();
  146. let boundary = random_boundary();
  147. let boundary_sep = format!("\r\n--{boundary}\r\n");
  148. let boundary_closer = format!("\r\n--{boundary}\r\n");
  149. resp = resp.header(
  150. CONTENT_TYPE,
  151. format!("multipart/byteranges; boundary={boundary}"),
  152. );
  153. for (end, start) in ranges {
  154. // a new range is being written, write the range boundary
  155. buf.write_all(boundary_sep.as_bytes())?;
  156. // write the needed headers `Content-Type` and `Content-Range`
  157. buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())?;
  158. buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())?;
  159. // write the separator to indicate the start of the range body
  160. buf.write_all("\r\n".as_bytes())?;
  161. // calculate number of bytes needed to be read
  162. let bytes_to_read = end + 1 - start;
  163. let mut local_buf = vec![0_u8; bytes_to_read as usize];
  164. file.seek(SeekFrom::Start(start))?;
  165. file.read_exact(&mut local_buf)?;
  166. buf.extend_from_slice(&local_buf);
  167. }
  168. // all ranges have been written, write the closing boundary
  169. buf.write_all(boundary_closer.as_bytes())?;
  170. resp.body(buf)
  171. }
  172. } else {
  173. resp = resp.header(CONTENT_LENGTH, len);
  174. let mut buf = Vec::with_capacity(len as usize);
  175. file.read_to_end(&mut buf)?;
  176. resp.body(buf)
  177. };
  178. http_response.map_err(Into::into)
  179. }
  180. fn random_boundary() -> String {
  181. let mut x = [0_u8; 30];
  182. getrandom::getrandom(&mut x).expect("failed to get random bytes");
  183. (x[..])
  184. .iter()
  185. .map(|&x| format!("{x:x}"))
  186. .fold(String::new(), |mut a, x| {
  187. a.push_str(x.as_str());
  188. a
  189. })
  190. }