video_stream.rs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. //! Using `wry`'s http module, we can stream a video file from the local file system.
  2. //!
  3. //! You could load in any file type, but this example uses a video file.
  4. use dioxus::desktop::wry::http;
  5. use dioxus::desktop::wry::http::Response;
  6. use dioxus::desktop::{use_asset_handler, AssetRequest};
  7. use dioxus::prelude::*;
  8. use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
  9. use std::{io::SeekFrom, path::PathBuf};
  10. use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
  11. const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
  12. fn main() {
  13. // For the sake of this example, we will download the video file if it doesn't exist
  14. ensure_video_is_loaded();
  15. dioxus::LaunchBuilder::desktop().launch(app);
  16. }
  17. fn app() -> Element {
  18. // Any request to /videos will be handled by this handler
  19. use_asset_handler("videos", move |request, responder| {
  20. // Using spawn works, but is slower than a dedicated thread
  21. tokio::task::spawn(async move {
  22. let video_file = PathBuf::from(VIDEO_PATH);
  23. let mut file = tokio::fs::File::open(&video_file).await.unwrap();
  24. match get_stream_response(&mut file, &request).await {
  25. Ok(response) => responder.respond(response),
  26. Err(err) => eprintln!("Error: {}", err),
  27. }
  28. });
  29. });
  30. rsx! {
  31. div {
  32. video {
  33. src: "/videos/test_video.mp4",
  34. autoplay: true,
  35. controls: true,
  36. width: 640,
  37. height: 480
  38. }
  39. }
  40. }
  41. }
  42. /// This was taken from wry's example
  43. async fn get_stream_response(
  44. asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
  45. request: &AssetRequest,
  46. ) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
  47. // get stream length
  48. let len = {
  49. let old_pos = asset.stream_position().await?;
  50. let len = asset.seek(SeekFrom::End(0)).await?;
  51. asset.seek(SeekFrom::Start(old_pos)).await?;
  52. len
  53. };
  54. let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
  55. // if the webview sent a range header, we need to send a 206 in return
  56. // Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
  57. let http_response = if let Some(range_header) = request.headers().get("range") {
  58. let not_satisfiable = || {
  59. ResponseBuilder::new()
  60. .status(StatusCode::RANGE_NOT_SATISFIABLE)
  61. .header(CONTENT_RANGE, format!("bytes */{len}"))
  62. .body(vec![])
  63. };
  64. // parse range header
  65. let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) {
  66. ranges
  67. .iter()
  68. // map the output back to spec range <start-end>, example: 0-499
  69. .map(|r| (r.start, r.start + r.length - 1))
  70. .collect::<Vec<_>>()
  71. } else {
  72. return Ok(not_satisfiable()?);
  73. };
  74. /// The Maximum bytes we send in one range
  75. const MAX_LEN: u64 = 1000 * 1024;
  76. if ranges.len() == 1 {
  77. let &(start, mut end) = ranges.first().unwrap();
  78. // check if a range is not satisfiable
  79. //
  80. // this should be already taken care of by HttpRange::parse
  81. // but checking here again for extra assurance
  82. if start >= len || end >= len || end < start {
  83. return Ok(not_satisfiable()?);
  84. }
  85. // adjust end byte for MAX_LEN
  86. end = start + (end - start).min(len - start).min(MAX_LEN - 1);
  87. // calculate number of bytes needed to be read
  88. let bytes_to_read = end + 1 - start;
  89. // allocate a buf with a suitable capacity
  90. let mut buf = Vec::with_capacity(bytes_to_read as usize);
  91. // seek the file to the starting byte
  92. asset.seek(SeekFrom::Start(start)).await?;
  93. // read the needed bytes
  94. asset.take(bytes_to_read).read_to_end(&mut buf).await?;
  95. resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
  96. resp = resp.header(CONTENT_LENGTH, end + 1 - start);
  97. resp = resp.status(StatusCode::PARTIAL_CONTENT);
  98. resp.body(buf)
  99. } else {
  100. let mut buf = Vec::new();
  101. let ranges = ranges
  102. .iter()
  103. .filter_map(|&(start, mut end)| {
  104. // filter out unsatisfiable ranges
  105. //
  106. // this should be already taken care of by HttpRange::parse
  107. // but checking here again for extra assurance
  108. if start >= len || end >= len || end < start {
  109. None
  110. } else {
  111. // adjust end byte for MAX_LEN
  112. end = start + (end - start).min(len - start).min(MAX_LEN - 1);
  113. Some((start, end))
  114. }
  115. })
  116. .collect::<Vec<_>>();
  117. let boundary = format!("{:x}", rand::random::<u64>());
  118. let boundary_sep = format!("\r\n--{boundary}\r\n");
  119. let boundary_closer = format!("\r\n--{boundary}\r\n");
  120. resp = resp.header(
  121. CONTENT_TYPE,
  122. format!("multipart/byteranges; boundary={boundary}"),
  123. );
  124. for (end, start) in ranges {
  125. // a new range is being written, write the range boundary
  126. buf.write_all(boundary_sep.as_bytes()).await?;
  127. // write the needed headers `Content-Type` and `Content-Range`
  128. buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
  129. .await?;
  130. buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
  131. .await?;
  132. // write the separator to indicate the start of the range body
  133. buf.write_all("\r\n".as_bytes()).await?;
  134. // calculate number of bytes needed to be read
  135. let bytes_to_read = end + 1 - start;
  136. let mut local_buf = vec![0_u8; bytes_to_read as usize];
  137. asset.seek(SeekFrom::Start(start)).await?;
  138. asset.read_exact(&mut local_buf).await?;
  139. buf.extend_from_slice(&local_buf);
  140. }
  141. // all ranges have been written, write the closing boundary
  142. buf.write_all(boundary_closer.as_bytes()).await?;
  143. resp.body(buf)
  144. }
  145. } else {
  146. resp = resp.header(CONTENT_LENGTH, len);
  147. let mut buf = Vec::with_capacity(len as usize);
  148. asset.read_to_end(&mut buf).await?;
  149. resp.body(buf)
  150. };
  151. http_response.map_err(Into::into)
  152. }
  153. fn ensure_video_is_loaded() {
  154. let video_file = PathBuf::from(VIDEO_PATH);
  155. if !video_file.exists() {
  156. tokio::runtime::Runtime::new()
  157. .unwrap()
  158. .block_on(async move {
  159. println!("Downloading video file...");
  160. let video_url =
  161. "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
  162. let mut response = reqwest::get(video_url).await.unwrap();
  163. let mut file = tokio::fs::File::create(&video_file).await.unwrap();
  164. while let Some(chunk) = response.chunk().await.unwrap() {
  165. file.write_all(&chunk).await.unwrap();
  166. }
  167. });
  168. }
  169. }