如果,后端有个接口,已经实现了视频分片,前端只需要将后端的api地址交给src,然后视频组件react-player就能够帮我们发起请求,然后进行播放,这导致视频进度条不管是前进还是后退都会发起很多垃圾请求。比如:
此时,向后端发起请求的是html5的video,不是开发者,所以不管你怎么做,你都拿不到发起请求的控制权。
现在你一定想到了前端的大文件分片下载:
大文件分片下载的核心理念是:juejin.cn/post/738180… 拿到文件,然后后端通过Content-range来设置视频片段,其实具体你现在要拿哪个片段,是前端控制的,后端只需要支持range就好了
Content-range:bytes 0-102131607/102131608
结合图片的分片下载
我写出了视频加载的后端代码:
router.get('/getFmp4', async (ctx) => {
const stat = fs.statSync(path.join(__dirname, 'fmp4.mp4'))
const range = ctx.req.headers.range
const parts = range.replace(/bytes=/, '').split('-')
const start = Number(parts[0])
const end = Number(parts[1]) || stat.size - 1
ctx.set('Content-Range', `bytes ${start}-${end}/${stat.size}`)
ctx.type = 'video/mp4'
ctx.set('Accept-Ranges', 'bytes')
ctx.status = 206
const stream = fs.createReadStream(path.join(__dirname, 'fmp4.mp4'), {
start,
end
})
ctx.body = stream
})
后端接口已经写好,大视频分块加载的方式只有2种:Blob和MediaDSource。
一.用Bolb实现一个react组件:
blob表示
二进制大对象,是JavaScript对不可修改二进制数据的封装类型。包含字符串的数组、ArrayBuffer、ArrayBufferViews,甚至其他Blob都可以用来创建blob。
Blob(Binary Large Object)是一种数据类型,表示一个不可变的、原始数据的类文件对象。它通常用于存储二进制数据,如图像、音频、视频文件,以及其他类似的数据。
直接上代码:
import React, { useEffect, useRef, useState } from 'react';
import ReactPlayer from 'react-player';
export const VideoPlayer = ({ videoSrc, chunkSize = 10 * 1024 * 1024 }: Record<string, any>) => {
const [videoBlob, setVideoBlob] = useState<string | null>(null);
const [downloadProgress, setDownloadProgress] = useState(0);
const chunksRef = useRef<ArrayBuffer[]>([]);
const mediaSourceUrlRef = useRef<string | null>(null);
// 每下载一块就追加到 chunks 并更新 Blob URL
const appendChunkAndUpdateBlob = (newChunk: ArrayBuffer) => {
chunksRef.current.push(newChunk);
const blob = new Blob(chunksRef.current, { type: 'video/mp4' });
const newUrl = URL.createObjectURL(blob);
mediaSourceUrlRef.current = newUrl;
};
useEffect(() => {
const downloadChunks = async () => {
try {
// 获取视频总大小
const headResponse = await fetch(videoSrc, { method: 'HEAD' });
const totalSize = parseInt(headResponse.headers.get('Content-Length') || '0', 10);
if (!totalSize) throw new Error('无法获取视频大小');
// 计算分片数量
const chunkCount = Math.ceil(totalSize / chunkSize);
// 分片下载
for (let i = 0; i < chunkCount; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize - 1, totalSize - 1);
const response = await fetch(videoSrc, {
headers: { Range: `bytes=${start}-${end}` },
});
const arrayBuffer = await response.arrayBuffer();
// 追加分片并更新视频源
appendChunkAndUpdateBlob(arrayBuffer);
// 更新进度
setDownloadProgress(Math.round(((i + 1) / chunkCount) * 100));
}
// 合并分片并创建URL
const blob = new Blob(chunksRef.current, { type: 'video/mp4' });
const finalUrl = URL.createObjectURL(blob);
setVideoBlob(finalUrl);
} catch (error) {
console.error('分片下载失败:', error);
}
};
downloadChunks();
return () => {
if (mediaSourceUrlRef.current) {
URL.revokeObjectURL(mediaSourceUrlRef.current);
}
if (videoBlob) {
URL.revokeObjectURL(videoBlob);
}
};
}, [videoSrc, chunkSize]);
console.log(mediaSourceUrlRef.current, 98811);
console.log(videoBlob, 9888);
return (
<div>
{downloadProgress < 100 && <div>下载进度: {downloadProgress}%</div>}
{mediaSourceUrlRef.current && (
<ReactPlayer
src={mediaSourceUrlRef.current}
controls
playing={true}
width="100%"
height="auto"
/>
)}
</div>
);
};
他的解题思路是:我用for循环,向后端发起请求,然后将请求到的chunk放到一个数组里面接收,己收到的值我就用Blob把他们拼接起来,然后把内存地址丢给组件。
理想很丰满,现实很骨感,所有的视频肯定得有一项基础功能就是:边下载边播放。
我认为此时的mediaSourceUrlRef.current内存地址是一个定值,他会帮我实现边下载边播放。
但是实际情况是,整个视频组件一直在抖动,你根本就没有机会点击播放按钮,不过等所有的视频都下载完毕,我们的视频就能正常播放了。
为啥会抖动呢?因为你每次都在给src赋值,你每赋值一次,他就会刷新一次,如果你的网速比较快,下载快,就是连点击播放按钮的机会都没有。如果你的网速比较慢,是可以播放的,但是是都一直在晃。
看到这里你一定会说:“草泥马,这干了个啥么。” 是的,我就是这种反应。一个视频等分片下载结束以后才能播放,还不如你直接下来到本地,然后播放呢。多此一举么。
结论:Blob分片拼接的办法并不能实现视频边下载边播放的功能。
二.MediaSource(多媒体流)
MediaSource是一个 Web API,用于在浏览器中动态生成媒体流,从而实现实时音频和视频流的播放。它允许您通过 JavaScript 代码来控制媒体数据的生成和传输,从而创建自定义的流媒体播放体验。
主要用途之一是实现流媒体的逐段加载,这对于大型视频、直播等场景非常有用。通过
MediaSource,您可以控制媒体片段的加载、缓冲和播放,从而实现更灵活的流媒体处理。
现在很多视频网站都用的 MediaSource 实现的大文件分片下载去节流的。比如:B站
import React, { useEffect, useRef, useState } from 'react';
import ReactPlayer from 'react-player';
export const VideoPlayer = ({ videoSrc, chunkSize = 10 * 1024 * 1024 }: Record<string, any>) => {
const mediaSourceRef = useRef<MediaSource | null>(null);
const sourceBufferRef = useRef<SourceBuffer | null>(null);
const chunksRef = useRef<ArrayBuffer[]>([]);
const [videoUrl, setVideoUrl] = useState<string | null>(null);
const [downloadProgress, setDownloadProgress] = useState(0);
// 初始化 MediaSource
useEffect(() => {
if (!window.MediaSource) {
console.error('当前浏览器不支持 MediaSource');
return;
}
const mediaSource = new MediaSource();
mediaSourceRef.current = mediaSource;
const url = URL.createObjectURL(mediaSource);
setVideoUrl(url);
mediaSource.addEventListener('sourceopen', handleSourceOpen);
}, []);
// 创建 SourceBuffer 并开始下载第一个分片
const handleSourceOpen = () => {
if (!mediaSourceRef.current) return;
try {
const sourceBuffer = mediaSourceRef.current.addSourceBuffer(
'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'
);
sourceBufferRef.current = sourceBuffer;
sourceBuffer.addEventListener('updateend', downloadNextChunk);
downloadNextChunk();
} catch (err) {
console.error('创建 SourceBuffer 失败:', err);
}
};
// 下载并追加下一个分片
const downloadNextChunk = async () => {
const currentChunk = chunksRef.current.length;
const totalSize = await getVideoTotalSize();
if (currentChunk >= Math.ceil(totalSize / chunkSize)) {
mediaSourceRef.current?.endOfStream();
return;
}
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize - 1, totalSize - 1);
const response = await fetch(videoSrc, {
headers: { Range: `bytes=${start}-${end}` },
});
const arrayBuffer = await response.arrayBuffer();
if (sourceBufferRef.current && !sourceBufferRef.current.updating) {
sourceBufferRef.current.appendBuffer(arrayBuffer);
chunksRef.current.push(arrayBuffer);
console.log(currentChunk, Math.ceil(totalSize / chunkSize), 78888);
setDownloadProgress(
Math.round(((currentChunk + 1) / Math.ceil(totalSize / chunkSize)) * 100)
);
}
};
// 获取视频总大小
const getVideoTotalSize = async (): Promise<number> => {
const headResponse = await fetch(videoSrc, { method: 'HEAD' });
const contentLength = headResponse.headers.get('Content-Length');
if (!contentLength) throw new Error('无法获取视频大小');
return parseInt(contentLength, 10);
};
return (
<div style={{ maxWidth: '800px', margin: '20px auto' }}>
{downloadProgress < 100 && <div>下载进度:{downloadProgress}%</div>}
{videoUrl && (
<ReactPlayer src={videoUrl} controls playing={true} width="100%" height="auto" />
)}
</div>
);
};
你以为大功告成了吗?想多了,控制台报错了。
你以为它会像所有的博主说的那样丝滑吗?no
MediaSource对视频类型有限制,你普通的mp4是加载不了的,他要的视频是标准 ISO BMF 格式的 MP4。
处理办法就是利用ffmpeg,把你的视频格式化一下就能播放了。
[ffmpeg 官网](https://ffmpeg.org/ffmpeg.html)
三.懒人办法
如果你不想写代码又想用Media Source实现分片下载,把下载的控制权掌握到开发者的手里,那就用知乎的这个包:https://github.com/zhihu/griffith/blob/master/packages/griffith-mp4/src/VideoComponent.tsx
推荐文章: 基于 HTTP Range 实现视频分片快速播放! (url)