Windows下Rust编码实现MP4点播服务器
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;