Appearance
05_课件 Manifest 与前端示例
什么是 Manifest?
Manifest 是后端在上传后“预处理”生成的一份 JSON 描述文件,用于告诉前端:
- 这个课件的类型(document 或 video)
- 如果是文档:总页数、每一页的可直接访问的图片 URL 列表
- 如果是视频:可直接播放的 HLS 播放列表
m3u8的 URL
这样前端无需下载并本地解析 PDF/PPT/视频,直接按 Manifest 渐进加载即可,首屏更快、设备更省电。
后端接口:
- 上传课件:
POST /upload/{course_id}/courseware- 支持
.pdf .ppt .pptx .mp4 .mov .mkv .webm - 返回
courseware_url(原文件路径)和manifest_url
- 支持
- 拉取 Manifest:
GET /upload/{course_id}/courseware/manifest- 可加
?asset_id=...指定某一次上传的资产版本;不传默认取最新
- 可加
Manifest JSON 示例
文档型(PDF/PPT 转页图):
json
{
"type": "document",
"page_count": 3,
"pages": [
{ "index": 1, "url": "/static/courseware/12/ab12cd/pages/0001.jpg" },
{ "index": 2, "url": "/static/courseware/12/ab12cd/pages/0002.jpg" },
{ "index": 3, "url": "/static/courseware/12/ab12cd/pages/0003.jpg" }
]
}视频型(生成 HLS):
json
{
"type": "video",
"hls_url": "/static/courseware/12/ef34gh/hls/index.m3u8"
}React 前端示例
以下是一个最小可用的 React 组件,自动请求 Manifest,根据类型展示文档分页或视频播放。
依赖:
- 文档:无额外依赖(图片懒加载即可)
- 视频:
hls.js(npm i hls.js)
tsx
import React, { useEffect, useMemo, useRef, useState } from 'react';
import Hls from 'hls.js';
type DocumentManifest = {
type: 'document';
page_count: number;
pages: { index: number; url: string }[];
};
type VideoManifest = {
type: 'video';
hls_url: string;
};
type Manifest = DocumentManifest | VideoManifest;
interface Props {
courseId: number;
backendBase: string; // 如 http://localhost:8000
assetId?: string; // 可选,指定某次上传版本
}
export const CoursewareViewer: React.FC<Props> = ({ courseId, backendBase, assetId }) => {
const [manifest, setManifest] = useState<Manifest | null>(null);
const [error, setError] = useState<string | null>(null);
const videoRef = useRef<HTMLVideoElement | null>(null);
useEffect(() => {
const controller = new AbortController();
const url = new URL(`/upload/${courseId}/courseware/manifest`, backendBase);
if (assetId) url.searchParams.set('asset_id', assetId);
fetch(url.toString(), { signal: controller.signal })
.then(async (r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then((data) => setManifest(data))
.catch((e) => setError(e.message));
return () => controller.abort();
}, [courseId, backendBase, assetId]);
useEffect(() => {
if (!manifest || manifest.type !== 'video') return;
const video = videoRef.current;
if (!video) return;
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(new URL(manifest.hls_url, backendBase).toString());
hls.attachMedia(video);
return () => hls.destroy();
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 原生支持 HLS
video.src = new URL(manifest.hls_url, backendBase).toString();
}
}, [manifest, backendBase]);
if (error) return <div>加载失败:{error}</div>;
if (!manifest) return <div>加载中…</div>;
if (manifest.type === 'document') {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{manifest.pages.map((p) => (
<img
key={p.index}
src={new URL(p.url, backendBase).toString()}
alt={`page-${p.index}`}
loading="lazy"
style={{ width: '100%', height: 'auto', background: '#f4f4f4' }}
/>
))}
</div>
);
}
// 视频
return (
<video ref={videoRef} controls style={{ width: '100%', maxHeight: 600 }} />
);
};使用示例:
tsx
<CoursewareViewer courseId={12} backendBase={"http://localhost:8000"} />前端性能建议
- 文档分页:按页懒加载,进入时预加载下一页;移动端可先加载低清晰度缩略图,再渐进替换高清。
- 视频:开启
hls.js的低延迟选项或使用多码率转码;弱网自动降码率。 - 静态文件交给 Nginx/CDN:配置强缓存(带指纹的路径可
immutable)。