Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

本文转载于稀土掘金技术社区,作者:爱吃橘子罐头

https://juejin.cn/post/7506790578107203603

最近有个需求要加载长视频,用户反馈视频加载慢、播放卡顿。为了解决这个问题,我研究了两种技术方案:Range + fmp4 + MediaSource 和 M3u8 切片方案。这两种方案各有优缺点,下面详细聊聊我的实现思路和踩过的坑。


方案一:Range + fmp4 + MediaSource

这个方案的核心思路是分段加载视频内容,而不是一次性加载整个视频文件。这样用户在观看前几秒内容时,后面的部分已经在后台默默加载了。

关键技术点拆解

1. Range 请求头

通过 Range 请求头告诉服务端:“我只需要文件的某一部分”。比如:

  • Range: bytes=200-1000:要第 200 到 1000 字节

  • Range: bytes=200-:从 200 字节一直要到最后

  • Range: bytes=-1000:只要最后 1000 字节

服务端需要支持范围请求:

  • 响应 206 Partial Content 表示成功返回部分内容

  • 如果请求范围不合法(比如超过文件大小),返回 416 Range Not Satisfiable

  • 如果服务端不处理 Range 请求,直接返回 200 和整个文件

2. fmp4 格式

普通 MP4 文件的元数据(比如视频时长、分辨率)集中在文件头部,如果只加载中间片段,播放器可能无法解析。而 fmp4(Fragmented MP4)  将元数据分散到各个片段,每个片段都能独立播放。

3. MediaSource API

浏览器提供的 MediaSource 对象允许我们动态拼接视频片段。大致流程:

  1. 创建 MediaSource 实例,生成一个虚拟的媒体 URL

  2. 监听 sourceopen 事件,创建 SourceBuffer 用于接收数据

  3. 分片请求视频内容,通过 appendBuffer 添加到 SourceBuffer

  4. 所有片段加载完成后,调用 endOfStream

后端实现(Express)

关键点在于处理 Range 请求头,直接用 Express 的 res.download 方法即可自动处理范围请求:

app.get('/getMp4/demo', (req, res) => {    res.download(        path.join(__dirname, '/static/demo.mp4'),        {             acceptRanges: true// 开启范围请求支持        }    )})

前端实现(Vue3)

前端需要注意动态获取视频总大小,而不是写死:

const videoSrc = ref('')const getRangeVideo = () => {// 应该从第一次请求的响应头中获取总大小// 比如 Content-Range: bytes 0-999/5524488const totalSize = 5524488const chunkSize = 1000000// 每次加载1MBconst mediaSource = new MediaSource()    videoSrc.value = URL.createObjectURL(mediaSource)    mediaSource.addEventListener('sourceopen', () => {const sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E"')const sendRequest = () => {if (startByte >= totalSize) return            axios({                url: '/getMp4/demo',                headers: { Range: `bytes=${startByte}-${startByte + chunkSize}` }            }).then(res => {                sourceBuffer.appendBuffer(res.data)                startByte += chunkSize + 1                setTimeout(sendRequest, 500) // 控制加载频率            })        }        sendRequest()        sourceBuffer.addEventListener('updateend', function () {if (startByte >= totalSize) mediaSource.endOfStream()        })    })}

方案二:M3u8 切片方案

这个方案更 “省心”,利用成熟的流媒体协议 HLS(HTTP Live Streaming),将视频切分为多个 .ts 文件,通过 .m3u8 索引文件控制播放顺序。

实现步骤

1. 使用 FFmpeg 切片

本地安装 FFmpeg 后,执行命令将视频转为 HLS 格式:

ffmpeg -i input.mp4 \       -c:v libx264 -an \       -hls_time 5 \      # 每段5秒       -hls_list_size 0 \  # m3u8保留所有片段信息       output.m3u8

生成的文件结构:

  • output.m3u8:索引文件,记录每个. ts 片段的信息

  • output0.tsoutput1.ts...:实际视频片段

然后将切片之后的文件放到前端能够访问的地方就行。放在前端项目代码内都行,比如public等文件夹内。

需要注意m3u8文件和所有切片文件都要放在同一个目录下。

2. 前端使用 video.js 播放

安装 video.js 后在页面使用

<template><divid="playerWrapper"><videoclass="video-js"controls></video></div></template><scriptsetup>import videoJs from'video.js'import lang_zhCn from'video.js/dist/lang/zh-CN.json'import'video.js/dist/video-js.min.css'videoJs.addLanguage('zh-CN', lang_zhCn)const myVideo = ref(null)onMounted(() => {    myVideo.value = videoJs('videoId', {sources: [{ src: '/videos/output.m3u8', type: 'application/x-mpegURL' }],autoplay: true,controls: true    })})</script>

不了解video.js的可以去翻阅下面的两个文档:

  • Video.js api 文档 [1]

  • Video.js 选项文档 [2]


两种方案对比

对比项
Range + fmp4 方案
M3u8 切片方案
实现复杂度
需要手动处理分片和拼接
依赖 FFmpeg 和 video.js,配置简单
播放体验
拖动进度条可能需重新加载
拖动流畅,自动切换清晰度
适用场景
需要精细控制加载逻辑的项目
快速上线、对体验要求高的场景

最后的选择

如果项目周期紧张,推荐直接用 M3u8 方案,毕竟 FFmpeg 和 video.js 的生态更成熟。但如果需要深度定制加载策略(比如根据网络速度动态调整分片大小),Range + fmp4 方案 会更灵活。