main.rs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. // Copyright 2019-2023 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. fn main() {
  6. use std::{
  7. cmp::min,
  8. io::{Read, Seek, SeekFrom},
  9. path::PathBuf,
  10. process::{Command, Stdio},
  11. };
  12. use tauri::http::{HttpRange, ResponseBuilder};
  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_uri_scheme_protocol("stream", move |_app, request| {
  35. // prepare our response
  36. let mut response = ResponseBuilder::new();
  37. // get the file path
  38. let path = request.uri().strip_prefix("stream://localhost/").unwrap();
  39. let path = percent_encoding::percent_decode(path.as_bytes())
  40. .decode_utf8_lossy()
  41. .to_string();
  42. if path != "example/test_video.mp4" {
  43. // return error 404 if it's not out video
  44. return response.mimetype("text/plain").status(404).body(Vec::new());
  45. }
  46. // read our file
  47. let mut content = std::fs::File::open(&video_file)?;
  48. let mut buf = Vec::new();
  49. // default status code
  50. let mut status_code = 200;
  51. // if the webview sent a range header, we need to send a 206 in return
  52. // Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
  53. if let Some(range) = request.headers().get("range") {
  54. // Get the file size
  55. let file_size = content.metadata().unwrap().len();
  56. // we parse the range header with tauri helper
  57. let range = HttpRange::parse(range.to_str().unwrap(), file_size).unwrap();
  58. // let support only 1 range for now
  59. let first_range = range.first();
  60. if let Some(range) = first_range {
  61. let mut real_length = range.length;
  62. // prevent max_length;
  63. // specially on webview2
  64. if range.length > file_size / 3 {
  65. // max size sent (400ko / request)
  66. // as it's local file system we can afford to read more often
  67. real_length = min(file_size - range.start, 1024 * 400);
  68. }
  69. // last byte we are reading, the length of the range include the last byte
  70. // who should be skipped on the header
  71. let last_byte = range.start + real_length - 1;
  72. // partial content
  73. status_code = 206;
  74. // Only macOS and Windows are supported, if you set headers in linux they are ignored
  75. response = response
  76. .header("Connection", "Keep-Alive")
  77. .header("Accept-Ranges", "bytes")
  78. .header("Content-Length", real_length)
  79. .header(
  80. "Content-Range",
  81. format!("bytes {}-{}/{}", range.start, last_byte, file_size),
  82. );
  83. // FIXME: Add ETag support (caching on the webview)
  84. // seek our file bytes
  85. content.seek(SeekFrom::Start(range.start))?;
  86. content.take(real_length).read_to_end(&mut buf)?;
  87. } else {
  88. content.read_to_end(&mut buf)?;
  89. }
  90. }
  91. response.mimetype("video/mp4").status(status_code).body(buf)
  92. })
  93. .run(tauri::generate_context!(
  94. "../../examples/streaming/tauri.conf.json"
  95. ))
  96. .expect("error while running tauri application");
  97. }
  98. // returns the scheme and the path of the video file
  99. // we're using this just to allow using the custom `stream` protocol or tauri built-in `asset` protocol
  100. #[tauri::command]
  101. fn video_uri() -> (&'static str, std::path::PathBuf) {
  102. #[cfg(feature = "protocol-asset")]
  103. {
  104. let mut path = std::env::current_dir().unwrap();
  105. path.push("test_video.mp4");
  106. ("asset", path)
  107. }
  108. #[cfg(not(feature = "protocol-asset"))]
  109. ("stream", "example/test_video.mp4".into())
  110. }