文件选择、上传与媒体播放
建立文件身份和任务状态,处理上传进度、取消、续传、媒体资源与实例清理。
本地文件如何进入服务
文件选择器或拖拽区取得 File,保存名字、类型、大小与字节。react-dropzone 组织选择与拖拽接口,不自行完成上传。客户端 accept、扩展名和 MIME 提供预检及提示,服务端仍需验证权限、大小、格式和实际内容。react-dropzone
一个上传任务需要稳定本地身份,文件名可能重复,不能单独用作进度或取消索引。多个任务应分别保存传输对象、进度、错误与结果,并限制并发;共用一个 AbortController 会只控制最后关联的任务。
进度说明到了哪一步
XHR 的上传进度表示客户端发送字节的观察。它没有证明服务器保存完成或后续处理可用。接收响应后解析状态与数据契约,只有服务器确认的结果才能接到存储对象;转码、抽取和审核可能继续进行。
已选择 → 等待传输 → 传输中 → 服务已接收 → 处理中 → 可使用
↘ 传输失败 ↘ 处理失败该图是教学状态模型。实际协议可以合并部分状态,页面仍要说明完成点。HTTP 2xx 需要结合响应契约解释;状态 0、网络错误和解析失败各有不同处理。
取消先检查信号是否已被取消,再登记监听,并在完成后移除监听。中止传输可能留下服务端部分文件或已完成效果,需要上传身份和清理规则。自动重试写入需要同一意图的可恢复契约,不能只打开查询库 retry。
以下实现把一个文件接到 XHR、进度、取消和结果解析。它假设同源 /api/uploads 返回 { id, state },state 为 processing 或 ready;服务应自行验证文件。它不提供续传或自动重试,也未运行新的上传服务。
import { z } from 'zod';
const receiptSchema = z.object({
id: z.string().min(1),
state: z.enum(['processing', 'ready']),
});
export function uploadFile(
file: File,
signal: AbortSignal,
onProgress: (sent: number, total: number | null) => void,
) {
return new Promise<z.infer<typeof receiptSchema>>((resolve, reject) => {
if (signal.aborted) {
reject(new DOMException('Upload aborted', 'AbortError'));
return;
}
const xhr = new XMLHttpRequest();
let settled = false;
const finish = (action: () => void) => {
if (settled) return;
settled = true;
signal.removeEventListener('abort', abort);
action();
};
const abort = () => xhr.abort();
xhr.open('POST', '/api/uploads');
xhr.timeout = 60_000;
xhr.upload.onprogress = event => {
onProgress(event.loaded, event.lengthComputable ? event.total : null);
};
xhr.onload = () => finish(() => {
if (xhr.status < 200 || xhr.status >= 300) {
reject(new Error(`Upload rejected: ${xhr.status}`));
return;
}
try { resolve(receiptSchema.parse(JSON.parse(xhr.responseText))); }
catch (error) { reject(error); }
});
xhr.onerror = () => finish(() => reject(new Error('Upload network failure')));
xhr.ontimeout = () => finish(() => reject(new Error('Upload timeout')));
xhr.onabort = () => finish(() => reject(new DOMException('Upload aborted', 'AbortError')));
signal.addEventListener('abort', abort, { once: true });
const body = new FormData();
body.set('file', file, file.name);
try { xhr.send(body); }
catch (error) { finish(() => reject(error)); }
});
}任务管理层以 crypto.randomUUID() 等生成的本地 ID 建立一任务一控制器关系,调用前先登记,finally 移除;进度回调只更新对应 ID。FormData 的 Content-Type 与 boundary 由浏览器设置。60 秒是示例传输期限;收到 processing 后还需查询或订阅服务器状态,不能把 100% 字节进度显示成业务已完成。
大文件怎样恢复
Uppy 组织多来源、任务界面和插件,tus 提供偏移量等续传协议。浏览器重载后恢复需要原上传位置、文件识别和仍可取得的字节;文件指纹是恢复线索,不能代替目标授权。
分片和续传服务要确认当前偏移、总长度、期限、配额和完成条件。网络重试可能重复发送,接收端据协议处理。预签名直传还要限制对象键、方法、期限、内容和公开范围,完成后由服务确认对象是否满足业务要求。tus 协议
播放器如何连接 React
媒体容器、编码、传输和播放器分别承担包装、表示、取得字节和控制呈现。HLS 清单与分片可以由浏览器原生支持或 hls.js 等引擎处理,具体方案按能力探测和目标设备核对;CORS 需覆盖清单、分片及密钥等实际取得路径。
video.js 或 hls.js 实例由 Effect 建立并保存在 ref,源改变时采用对应更新接口,卸载时 dispose 或 destroy。错误恢复需要限制次数和期限,不能在每次网络错误后无限 startLoad。重复开发初始化也要正确清理。
原生 play() 返回的 Promise 可能因策略或其他条件被拒绝。自动播放、静音与 playsInline 需要适配目标设备,控制界面应仍能让用户启动与暂停。字幕、替代描述、键盘及状态反馈按内容和用户需求提供。
若使用 hls.js,可在 React 中把 DOM 资源、HLS 实例与源绑定。此例在源改变时销毁并重建,适合展示所有权;保持播放位置或无缝切源要另按播放器接口设计。
import { useEffect, useRef } from 'react';
import Hls from 'hls.js';
export function HlsVideo({ src }: { src: string }) {
const ref = useRef<HTMLVideoElement>(null);
useEffect(() => {
const video = ref.current;
if (!video) return;
let engine: Hls | undefined;
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = src;
} else if (Hls.isSupported()) {
engine = new Hls();
engine.loadSource(src);
engine.attachMedia(video);
}
// 不支持时由能力状态在界面中提供文本和替代入口。
return () => {
engine?.destroy();
video.pause();
video.removeAttribute('src');
video.load();
};
}, [src]);
return <video ref={ref} controls playsInline />;
}原生媒体错误与 hls.js 的 ERROR 事件由界面分别订阅,显示可恢复原因;实例分支的详细事件没有在此片段注册。不支持时在父界面根据同样能力探测显示文字与替代入口。此例不自动播放、不无限恢复,未验证目标设备解码能力。hls.js 使用接口
可以验证什么
单元测试可以检查任务状态、身份、协议分支和错误映射;网络替身可以检查上传接口接线;实际浏览器验证才能观察选文件、取消、续传、媒体解码及焦点。归档样例没有在本次重组中变成新运行记录。video.js React 指导、hls.js
最后更新于