Rust編碼可以實現眾多簡潔、可靠、高效的應用,但語法邏輯要求嚴格,尤其是依賴庫的選擇調用,需要耐心堅持“推敲”。借助DeepSeek并反復編程調試和問答改進,可以最終得到完整有效的Rust編碼。下面分享Windows下Rust編碼實現MP4點播服務器設計實現。
1 整體規劃設計
1.1 功能分析設計
簡單MP4點播服務器,設計簡單html頁面進行播放文件的選擇和播放控制。
Rust后端編碼設計,采用簡易mp4庫,不選用FFmpeg,避免復雜的環境配置和開發過程中的過多“坑”險。
項目整體規劃如下:
- Rust后端:處理MP4文件讀取和HTTP服務
- 前端HTML頁面:遠程選擇播放文件和控制播放
- 輕量級架構:使用高效Rust庫實現核心功能
- 文中文件名稱的前后端支持
1.2 初始項目構建
把設想和功能要求,拋給AI-LLM工具,如騰迅的ima,借助內置的Hunyuan/DeepSeek,獲取初始的項目構造和必須的文件編碼,減輕初步完全編碼的困難。以此為參照,開始后續項目架構和編碼設計。關鍵的提問描述如下:
請為初學者給出Windows下Rust編碼的簡單MP4點播服務器,設計簡單html頁面進行遠程選擇播放文件并進行播放控制。不用FFMPEG。給出項目文件結構和完整編碼。
圖1是騰迅ima的運用組合截圖。
圖1 騰迅ima項目借力的運用組合截圖
2 項目構建準備
2.1 項目整體構建
適應Windows操作系統,選擇MinGW-GNU支持的Rust底層支持環境。簡化簡潔安裝,避免過多且易于沖突調整,這里采用離線版:rust-1.88.0-x86_64-pc-windows-gnu.msi,一步到位。
進而安裝采用RustRoverIDE集成開發環境,執照上述ima的設計指導,構建整個項目工程,框架結構如下文本框所示。
mp4-server/
├── Cargo.toml??????????? # 項目依賴配置
├── videos/?????????????? # 存放MP4視頻文件的目錄
├── static/?????????????? ?# 前端靜態文件
│?? └── index.html??????? ?????# 交互操控界面
└── src/
??? ├── main.rs?????????? ?????# 服務器入口
??? ├── handlers.rs?????? ??????# HTTP請求處理
??? └── mp4_utils.rs????? ??????# MP4文件處理工具
2.2 項目依賴管理
添加項目所需依賴--Cargo.toml依賴配置
[package]name = "mp4_server2"version = "0.1.0"edition = "2024"[dependencies]warp = "0.3"????????????? ?????????????????????# HTTP服務器框架
tokio = { version = "1", features = ["full"] } ?????????# 異步運行時
serde = { version = "1.0", features = ["derive"] } ?????# 序列化
serde_json = "1.0"??????? ??????????????????????# JSON處理
mp4 = "0.14.0"??????????? ?????????????????????# MP4文件處理庫
futures = "0.3"?????????? ???????????????????????# 異步支持
lazy_static = "1.4"regex = "1.11.1"?????? ?????????????????????????# 靜態初始化
encoding = "0.2.33"percent-encoding = "2.3.1"tokio-util = "0.7.16"hyper = "0.14.32"? ?????????????????????????????# 支持GBK/GB18030解碼
3 后端編碼實現
3.1 Mp4文件處理
src/mp4_utils.rs
use mp4::{Mp4Reader, Result}; use std::fs::File;
use std::io::{BufReader, Read, Seek, SeekFrom}; use std::path::Path;
// 獲取視頻文件元數據
pub fn get_video_metadata(path: &Path) -> Result<(u64, u32, u32)> {
??? let file = File::open(path)?;
??? let size = file.metadata()?.len();
??? let reader = BufReader::new(file);
??? let mp4 = Mp4Reader::read_header(reader, size)?;
??? let duration = mp4.duration().as_secs();
??? let width = mp4.tracks().values().next().map(|t| t.width()).unwrap_or(0);
??? let height = mp4.tracks().values().next().map(|t| t.height()).unwrap_or(0);
??? Ok((duration, width as u32, height as u32))
}
// 流式傳輸視頻文件
pub fn stream_video_file(path: &Path, start: u64, end: Option<u64>) -> std::io::Result<Vec<u8>> {
??? let mut file = File::open(path)?;
??? let file_size = file.metadata()?.len();
??? // 計算實際讀取范圍
??? let end = end.unwrap_or(file_size); let length = end - start;
??? // 定位并讀取數據
??? file.seek(SeekFrom::Start(start))?;
??? let mut buffer = vec![0; length as usize];
??? file.read_exact(&mut buffer)?;
??? Ok(buffer)
}
3.2 Http請求處理
src/handlers.rs
use warp::{Rejection, Reply};
use std::path::{PathBuf}; use std::fs; use std::ffi::OsString;
use encoding::{all::GB18030, EncoderTrap, Encoding};
use percent_encoding::{percent_decode, utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Serialize; use tokio_util::io::ReaderStream; use hyper::Body; use crate::mp4_utils;
#[derive(Serialize)]
struct VideoInfo { name: String, duration: u64, width: u32, height: u32, }
// 獲取視頻列表(兼容中文文件名)
pub async fn list_videos() -> Result<impl Reply, Rejection> {
??? let videos_dir = PathBuf::from("./videos"); let mut videos = Vec::new();
??? if let Ok(entries) = fs::read_dir(videos_dir) {
??????? for entry in entries.flatten() {
??????????? let path = entry.path();
??????????? if path.is_file() && path.extension().map_or(false, |ext| ext == "mp4") {
??????????????? // 使用OsString原生處理文件名(兼容GBK/UTF-8)
??????????????? let os_name: OsString = entry.file_name();
??????????????? let name = os_name.to_string_lossy().into_owned(); // 轉換為UTF-8字符串
??????????????? if let Ok((duration, width, height)) = mp4_utils::get_video_metadata(&path) {
??????????????????? videos.push(VideoInfo { name, duration, width, height });
??????????????? }
??????????? }
??????? }
??? }
??? Ok(warp::reply::json(&videos))
}
// 流式傳輸視頻
pub async fn stream_video(name: String, range: Option<String>) -> Result<impl Reply, Rejection> {
??? // 1. URL解碼文件名(前端encodeURIComponent的逆操作)
??? let decoded_name = percent_decode(name.as_bytes())
??????? .decode_utf8().map_err(|_| warp::reject::not_found())?
??????? .into_owned();
??? // 2. 將UTF-8轉換為GBK(Windows原生編碼)
??? let _gbk_bytes = GB18030.encode(&decoded_name, EncoderTrap::Strict)
??????? .map_err(|_| warp::reject::not_found())?;
??? // 3. 通過OsString構建路徑(關鍵修復)
??? // 使用OsString::from而不是from_vec
??? let os_name = OsString::from(decoded_name.clone());
??? let path = PathBuf::from("./videos").join(os_name);
??? // 4. 解析Range頭
??? let (start, end) = if let Some(range_header) = range {
??????? parse_range_header(&range_header, &path)?
??? } else {
??????? (0, None)
??? };
??? // 5. 異步讀取文件
??? let file = tokio::fs::File::open(&path).await
??????? .map_err(|_| warp::reject::not_found())?;
??? // 6. 創建流式響應體
??? let stream = ReaderStream::new(file); let body = Body::wrap_stream(stream);
??? // 7. 獲取文件元數據
??? let metadata = tokio::fs::metadata(&path).await.map_err(|_| warp::reject::not_found())?;
??? let file_size = metadata.len();
??? // 8. 構建Content-Range頭
??? let content_range = if let Some(end) = end {
??????? let end = end.min(file_size - 1);
??????? format!("bytes {}-{}/{}", start, end, file_size)
??? } else {
??????? format!("bytes {}-{}/{}", start, file_size - 1, file_size)
??? };
??? // 9. 設置響應頭(解決瀏覽器中文亂碼)
??? let safe_name = utf8_percent_encode(&decoded_name, NON_ALPHANUMERIC).to_string();
??? let content_disposition = format!(
??????? "attachment; filename=\"{}\"; filename*=utf-8''{}", safe_name, safe_name );
??? // 10. 構建響應,使用warp::reply::Response包裝Body
??? let mut response = warp::reply::Response::new(body);
??? let headers = response.headers_mut();
??? headers.insert("Content-Type", "video/mp4".parse().unwrap());
??? headers.insert("Content-Range", content_range.parse().unwrap());
??? headers.insert("Content-Disposition", content_disposition.parse().unwrap());
??? Ok(response)
}
// 解析Range頭
fn parse_range_header(range: &str, path: &PathBuf) -> Result<(u64, Option<u64>), Rejection> {
??? let file_size = fs::metadata(path).map_err(|_| warp::reject::not_found())?.len();
??? if let Some(captures) = regex::Regex::new(r"bytes=(\d+)-(\d*)").unwrap()captures(range) {
??????? let start = captures.get(1).unwrap().as_str().parse::<u64>()
??????????? .map_err(|_| warp::reject::not_found())?;
??????? let end = captures.get(2).unwrap().as_str();
??????? let end = if end.is_empty() {
??????????? None
??????? } else {
??????????? Some(end.parse::<u64>().map_err(|_| warp::reject::not_found())?.min(file_size - 1))
??????? };
??????? return Ok((start, end));
??? }
??? Err(warp::reject::not_found())
}
// 輔助函數:URL編碼轉換(供前端使用)
pub fn encode_filename(name: &str) -> String {
??? utf8_percent_encode(name, NON_ALPHANUMERIC).to_string()
}
3.3 服務器主入口
src/main.rs
mod handlers; mod mp4_utils;
use warp::Filter; use handlers::{list_videos, stream_video};
#[tokio::main]
async fn main() {
??? // 創建路由
??? let list_route = warp::path!("api" / "videos").and(warp::get()).and_then(list_videos);
??? let stream_route = warp::path!("api" / "videos" / String)
??????? .and(warp::get()).and(warp::header::optional("Range")).and_then(stream_video);
??? let static_files = warp::path("static").and(warp::fs::dir("./static"));
??? let index = warp::path::end().and(warp::fs::file("./static/index.html"));
??? // 組合所有路由
??? let routes = list_route.or(stream_route).or(static_files).or(index)
??????? .with(warp::cors().allow_any_origin());
??? // 啟動服務器
??? println!("Server started at http://localhost:9700");
??? warp::serve(routes).run(([0, 0, 0, 0], 9700)).await;
}
4 前端交互頁面設計
static/index.html
<!DOCTYPE html>
<html lang="en">
<head>
? <meta charset="UTF-8">
? <meta name="viewport" content="width=device-width, initial-scale=1.0">
? <title>MP4視頻服務器</title>
? <style>
??? :root { --primary-color: #3498db; --secondary-color: #2980b9;
????? --background-color: #f5f7fa; --card-bg: #ffffff; --text-color: #333333;
????? --border-color: #e0e0e0; --success-color: #2ecc71; --hover-color: #f1f9ff; }
??? * { margin: 0; padding: 0; box-sizing: border-box;
????? font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
??? body { background-color: var(--background-color);
????? color: var(--text-color); line-height: 1.6; padding: 2px; }
??? .container { max-width: 1200px; margin: 0 auto; display: grid;
????? grid-template-columns: 1fr 2fr; gap: 10px; }
??? @media (max-width: 768px) { .container { grid-template-columns: 1fr; } }
??? header { grid-column: 1 / -1; text-align: center; margin-bottom: 2px;
????? padding: 20px 0; border-bottom: 1px solid var(--border-color); }
??? h1 { color: var(--primary-color); font-size: 2.5rem; margin-bottom: 1px; }
??? .card { background: var(--card-bg); border-radius: 10px; padding: 25px;
????? box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); transition: transform 0.3s ease; }
??? .card:hover { transform: translateY(-5px); }
??? .video-list-container { height: 100%; display: flex; flex-direction: column; }
?? ?.video-list-header { display: flex; justify-content: space-between;
????? align-items: center; margin-bottom: 15px; padding-bottom: 10px;
????? border-bottom: 1px solid var(--border-color); }
??? .video-list-header h2 { font-size: 1.5rem; color: var(--primary-color); }
??? .video-count { background: var(--primary-color); color: white;
????? border-radius: 20px; padding: 2px 10px; font-size: 0.9rem; }
??? #videoList { list-style: none; max-height: 500px; overflow-y: auto;
????? flex-grow: 1; border: 1px solid var(--border-color);
????? border-radius: 8px; padding: 5px; }
??? #videoList::-webkit-scrollbar { width: 8px; }
??? #videoList::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
??? #videoList::-webkit-scrollbar-thumb { background: var(--primary-color); border-radius: 4px; }
??? #videoList li { padding: 12px 15px; border-bottom: 1px solid var(--border-color);
????? cursor: pointer; transition: background-color 0.2s; display: flex;
????? justify-content: space-between; align-items: center; }
??? #videoList li:last-child { border-bottom: none; }
??? #videoList li:hover { background-color: var(--hover-color); }
??? #videoList li.active { background-color: var(--primary-color); color: white; }
??? .video-duration { background: var(--secondary-color); color: white;
????? border-radius: 4px; padding: 2px 8px; font-size: 0.85rem; }
??? .player-container { display: flex; flex-direction: column; gap: 20px; }
??? .video-player-wrapper { position: relative; padding-bottom: 56.25%; /* 16:9 aspect ratio */
????? height: 0; overflow: hidden; border-radius: 8px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); }
??? #videoPlayer { position: absolute; top: 0; left: 0; width: 100%; height: 100%;background: #000; }
??? .controls { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; background:var(--card-bg);
????? padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); }
??? .btn { background: var(--primary-color); color: white; border: none; padding: 10px 15px;
????? border-radius: 5px; cursor: pointer; transition: background 0.3s;
????? font-weight: 600; display: flex; align-items: center; gap: 5px; }
??? .btn:hover { background: var(--secondary-color); }
??? .btn i { font-size: 1.2rem; }
??? .seek-container { flex-grow: 1; display: flex; align-items: center; gap: 10px;
??? #seekBar { flex-grow: 1; height: 8px; -webkit-appearance: none;
????? background: #e0e0e0; border-radius: 4px; outline: none; }
??? #seekBar::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px;
????? background: var(--primary-color); border-radius: 50%; cursor: pointer; }
??? .time-display { font-size: 0.9rem; color: #666; min-width: 80px; text-align: center; }
??? .search-container { margin-top: 15px; position: relative; }
??? #searchInput { width: 100%; padding: 10px 15px 10px 40px;
????? border: 1px solid var(--border-color); border-radius: 5px; font-size: 1rem; }
??? .search-icon { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #777; }
??? .empty-state { text-align: center; padding: 30px; color: #777; }
??? .empty-state i { font-size: 3rem; margin-bottom: 15px; color: #ddd; }
??? .video-info { display: flex; justify-content: space-between;
????? font-size: 0.9rem; color: #666; margin-top: 5px; }
? </style>
? <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="container">
? <header>
??? <h1><i class="fas fa-play-circle"></i>MP4視頻服務器</h1>
??? <p>流式傳輸管理MP4視頻流,愷肇乾,2025-08-06</p>
? </header>
? <div class="video-list-container card">
??? <div class="video-list-header">
????? <h2>有效視頻數</h2>
????? <span class="video-count">12 videos</span>
??? </div>
??? <div class="search-container">
????? <i class="fas fa-search search-icon"></i>
????? <input type="text" id="searchInput" placeholder="搜索視頻...">
??? </div>
??? <ul id="videoList">
????? <!-- Videos will be populated by JavaScript -->
????? <li>
??????? <span>Introduction to Rust Programming</span>
??????? <span class="video-duration">10:24</span>
??? ??</li>
????? <li class="active">
??????? <span>Building Web Servers with Rust</span>
??????? <span class="video-duration">15:42</span>
????? </li>
????? <li>
??????? <span>Concurrency in Rust Explained</span>
??????? <span class="video-duration">22:18</span>
????? </li>
????? <li>
??????? <span>Rust vs C++ Performance Comparison</span>
??????? <span class="video-duration">18:05</span>
????? </li>
????? <li>
??????? <span>Memory Management in Rust</span>
??????? <span class="video-duration">12:37</span>
????? </li>
????? <li>
??????? <span>Creating CLI Tools with Rust</span>
??????? <span class="video-duration">14:56</span>
????? </li>
????? <li>
??????? <span>WebAssembly with Rust Tutorial</span>
??????? <span class="video-duration">25:12</span>
????? </li>
????? <li>
??????? <span>Async Programming in Rust</span>
??????? <span class="video-duration">20:45</span>
????? </li>
????? <li>
??????? <span>Rust for Embedded Systems</span>
??????? <span class="video-duration">16:33</span>
????? </li>
????? <li>
??????? <span>Testing Strategies in Rust</span>
??????? <span class="video-duration">11:29</span>
????? </li>
????? <li>
??????? <span>Building REST APIs with Actix</span>
??????? <span class="video-duration">19:17</span>
????? </li>
????? <li>
??????? <span>Rust Macros Deep Dive</span>
??????? <span class="video-duration">17:48</span>
????? </li>
??? </ul>
? </div>
? <div class="player-container">
??? <div class="card">
????? <div class="video-player-wrapper">
??????? <video id="videoPlayer" controls poster="https://picsum.photos/800/450?random">
????????? <source src="" type="video/mp4">Your browser does not support the video tag.
??????? </video>
????? </div>
??? </div>
??? <div class="controls card">
????? <button id="playBtn" class="btn"><i class="fas fa-play"></i>播放</button>
????? <button id="pauseBtn" class="btn"><i class="fas fa-pause"></i>暫停</button>
????? <div class="seek-container">
??????? <span class="time-display" id="currentTime">0:00</span>
??????? <input type="range" id="seekBar" min="0" max="100" value="0">
??????? <span class="time-display" id="duration">0:00</span>
????? </div>
????? <button id="fullscreenBtn" class="btn"><i class="fas fa-expand"></i></button>
??? </div>
??? <div class="card">
????? <h3>現在正在播放: <span id="nowPlaying">Building Web Servers with Rust</span></h3>
????? <div class="video-info">
??????? <span>時長: <span id="videoDuration">15:42</span></span>
??????? <span>分辯率: 1080p</span>
??????? <span>大小: 未定 MB</span>
????? </div>
??? </div>
? </div>
</div>
</body>
<script>
? // 獲取視頻列表
? async function loadVideos() {
??? try {
????? const response = await fetch('/api/videos');
????? const videos = await response.json();
?? ???const listElement = document.getElementById('videoList');
????? const videoCount = document.querySelector('.video-count');
????? // 清空列表(除了示例項)
????? listElement.innerHTML = '';
????? if (videos.length === 0) {
??????? listElement.innerHTML = `
?????? ?????<div class="empty-state">
????????????? <i class="fas fa-film"></i>
????????????? <p>No videos found</p>
????????????? <p>Upload videos to the server to get started</p>
??????????? </div>
????????? `;
??????? videoCount.textContent = '0 個';
??????? return;
????? }
????? videoCount.textContent = `${videos.length} ${videos.length === 1 ? '個' : '個'}`;
????? videos.forEach(video => {
??????? const li = document.createElement('li');
??????? // 格式化時間為 MM:SS
??????? const minutes = Math.floor(video.duration / 60);
??????? const seconds = video.duration % 60;
??????? const formattedTime = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
??????? li.innerHTML = `
??????????? <span>${video.name}</span>
??????????? <span class="video-duration">${formattedTime}</span>
????????? `;
??????? li.onclick = () => {
????????? // 移除所有active類
????????? document.querySelectorAll('#videoList li').forEach(item => {
??????????? item.classList.remove('active');
????????? });
????? ????// 添加active類到當前項
????????? li.classList.add('active');
????????? playVideo(video.name);
??????? };
??????? listElement.appendChild(li);
????? });
????? // 默認選擇第一個視頻
????? if (videos.length > 0) {
??????? listElement.firstChild.classList.add('active');
??????? playVideo(videos[0].name);
????? }
??? } catch (error) {
????? console.error('Error loading videos:', error);
????? const listElement = document.getElementById('videoList');
????? listElement.innerHTML = `
????????? <div class="empty-state">
??????????? <i class="fas fa-exclamation-triangle"></i>
??????????? <p>Failed to load videos</p>
??????????? <p>Please check your connection</p>
????????? </div>
??????? `;
??? }
? }
? // 播放視頻
? function playVideo(name) {
??? const player = document.getElementById('videoPlayer');
??? const nowPlaying = document.getElementById('nowPlaying');
??? const encodedName = encodeURIComponent(name);
??? nowPlaying.textContent = name;
??? player.src = `/api/videos/${encodedName}`;
??? player.load();
? ??// 更新視頻信息
??? updateVideoInfo(name);
? }
? // 更新視頻信息(模擬)
? function updateVideoInfo(name) {
??? const durationElement = document.getElementById('videoDuration');
??? const videoItems = document.querySelectorAll('#videoList li');
??? // 在實際應用中,這里會從API獲取詳細信息
??? // 這里只是模擬從列表項中獲取時長
??? videoItems.forEach(item => {
????? if (item.textContent.includes(name)) {
??????? const durationSpan = item.querySelector('.video-duration');
??????? if (durationSpan) {
????????? durationElement.textContent = durationSpan.textContent;
??????? }
????? }
??? });
? }
? // 搜索功能
? function setupSearch() {
??? const searchInput = document.getElementById('searchInput');
??? searchInput.addEventListener('input', function() {
????? const searchTerm = this.value.toLowerCase();
????? const videoItems = document.querySelectorAll('#videoList li');
????? videoItems.forEach(item => {
??????? const videoName = item.querySelector('span:first-child').textContent.toLowerCase();
??????? if (videoName.includes(searchTerm)) {
????????? item.style.display = 'flex';
??????? } else {
????????? item.style.display = 'none';
??????? }
????? });
??? });
? }
? // 播放控制
? function setupPlayerControls() {
??? const player = document.getElementById('videoPlayer');
??? const playBtn = document.getElementById('playBtn');
??? const pauseBtn = document.getElementById('pauseBtn');
??? const seekBar = document.getElementById('seekBar');
??? const currentTime = document.getElementById('currentTime');
??? const duration = document.getElementById('duration');
??? const fullscreenBtn = document.getElementById('fullscreenBtn');
??? playBtn.addEventListener('click', () => {
????? player.play();
??? });
??? pauseBtn.addEventListener('click', () => {
????? player.pause();
??? });
??? // 更新進度條和時間顯示
??? player.addEventListener('timeupdate', () => {
????? const value = (player.currentTime / player.duration) * 100;
????? seekBar.value = isNaN(value) ? 0 : value;
????? // 更新時間顯示
????? currentTime.textContent = formatTime(player.currentTime);
??? });
??? // 視頻加載后更新總時長
??? player.addEventListener('loadedmetadata', () => {
????? duration.textContent = formatTime(player.duration);
??? });
??? seekBar.addEventListener('input', () => {
????? const time = player.duration * (seekBar.value / 100);
????? player.currentTime = time;
??? });
??? // 全屏功能
??? fullscreenBtn.addEventListener('click', () => {
????? if (player.requestFullscreen) {
??????? player.requestFullscreen();
????? } else if (player.mozRequestFullScreen) {
??????? player.mozRequestFullScreen();
????? } else if (player.webkitRequestFullscreen) {
??????? player.webkitRequestFullscreen();
????? } else if (player.msRequestFullscreen) {
??????? player.msRequestFullscreen();
????? }
??? });
? }
? // 格式化時間為 MM:SS
? function formatTime(seconds) {
??? const minutes = Math.floor(seconds / 60);
??? const secs = Math.floor(seconds % 60);
??? return `${minutes}:${secs < 10 ? '0' : ''}${secs}`;
? }
? // 初始化
? window.onload = function() {
??? loadVideos();
??? setupPlayerControls();
??? setupSearch();
? };
</script>
</html>
5 測調試部署運行
5.1 使用說明
- 創建項目結構并添加上述文件
- 在項目根目錄創建videos文件夾并放入MP4文件
- 運行服務器:cargo run
- 訪問 http://localhost:9700
圖2 瀏覽器交互運行及其跟蹤調試組合窗口截圖
5.2 IDE跟蹤調試
RustRoverIDE跟蹤調試組合窗口截圖,如圖3所示。
圖3 RustRoverIDE跟蹤調試組合窗口截圖
5.3 功能特點
- MP4文件處理:使用mp4-rust庫高效讀取MP4文件
- 流式傳輸:支持HTTP Range請求,實現視頻流式播放
- 元數據提取:自動獲取視頻時長和分辨率信息
- 響應式界面:簡潔的前端界面支持視頻選擇和播放控制
- 輕量級架構:無外部依賴,僅使用Rust標準庫和高效組件
5.4 部署運行
將生成的exe文件連同static、videos兩個目錄文件,一起放到服務器上,進行exe執行文件,即可遠程通過瀏覽器交互訪問了。
注意,生成exe文件時,主程序中的IP需是:“0,0,0,0”,不能再是“127.0.0.1”。
??? warp::serve(routes).run(([0, 0, 0, 0], 9700)).await;