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 使用说明

  1. 创建项目结构并添加上述文件
  2. 在项目根目录创建videos文件夹并放入MP4文件
  3. 运行服务器:cargo run
  4. 访问 http://localhost:9700

图2 浏览器交互运行及其跟踪调试组合窗口截图

5.2 IDE跟踪调试

RustRoverIDE跟踪调试组合窗口截图,如图3所示。

图3 RustRoverIDE跟踪调试组合窗口截图

5.3 功能特点

  1. MP4 文件处理:使用mp4-rust库高效读取MP4文件
  2. 流式传输:支持HTTP Range请求,实现视频流式播放
  3. 元数据提取:自动获取视频时长和分辨率信息
  4. 响应式界面:简洁的前端界面支持视频选择和播放控制
  5. 轻量级架构:无外部依赖,仅使用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;