一、前言
在当今数字时代,视频内容的传输和播放需求不断增长。本文将介绍一种创新的解决方案,以实现大视频分片上传和流畅播放。并会提供参考代码,帮助读者深入理解和实践。
- 开发背景:Web 端需要支持上传视频文件,大小限制最大为 1G。上传后播放器可以直接播放。
- 开发框架、库:
- Vue 2
- Axios
- TCPlayer
- 难点描述:
- 大视频分片上传
- 视频播放
二、普通视频直传
在前端实现视频上传的过程中,通常会使用 HTML5 中的 File API 和网络请求来完成文件的选择和上传。一般情况下,视频大小在 100M 以内可以考虑直接上传,不需要分片。
2.1 HTML 部分
HTML 中大多数情况会隐藏原生 Input 标签,然后自定义样式。用自己自定义的按钮点击模拟 Input 的点击出发 change 事件。
<style lang="less" scoped>
.upload-input {
display: none;
}
</style>
<template>
<input
class="upload-input"
type="file"
ref="file"
@change="handleFileChange"
accept="video/*"
/>
<div @click="handleFileSelect">视频上传</div>
</template>
2.2 JavaScript 部分
看看 js 的部分,主要是完成了文件的过滤和上传。
export default {
methods: {
// 选择文件
handleFileSelect() {
this.$refs.file.value = null;
this.$refs.file.click();
},
handleFileChange(e) {
// e.target.files 是一个类数组,转成真数组,可以将文件属性格式化,增加自定义属性
const files = Array.from(e.target.files);
// 这里可以根据文件的大小和类型过滤一下
const allowFiles = files.filter((item) => xxx === xxx);
audioFiles.forEach((file) => {
this.uploadVideo(file);
});
},
// 上传音视频
async uploadVideo(file) {
// 要和后台同学约定好上传视频的格式,这里是一个 post 请求把 formData 全部放在接口的 body 里
const fd = new FormData();
fd.append("file", file);
try {
const { data } = await axios.post("/api/videos/upload", fd, {
// 注意需要设置一下 Content-Type
headers: { "Content-Type": "multipart/form-data" },
// 网络比较差的情况下请求很容易超时,这里设置了超时时间为 2 分钟
timeout: 2 * 60 * 1000,
});
if (data.code === 0) {
// 更新上传状态为 “上传成功”
} else {
// 更新上传状态为 “上传失败”
}
} catch (error) {
console.error(error);
}
},
},
};
2.3 小结
- 定义一个文件选择输入框
input type="file"
和一个触发上传的按钮div
- 获取文件选择输入框中选中的视频文件
e.target.files
- 过滤掉大小和格式不符合要求的文件
- 创建
formData
,并且设置Content-Type
后发起上传请求
三、大视频分片上传
大视频主要指 1G 左右,甚至更大的视频。太大了,一般文件直传接口会超时,导致上传失败。通常会考虑将文件分片后上传,并做进度监控。
先简单看看效果:
3.1 文件流切片
文件流切片是一种将文件分割成较小的片段或块的技术。它在文件上传、大文件处理以及网络传输等场景中非常有用。通过将文件切片成较小的块,可以更高效地处理和传输文件数据。
3.1.1 定义切片大小
这里文件流存储以“字节”(Byte)为单位。那么 1MB
就是 1024 * 1024
// 切片大小(1MB)
const chunkSize = 1024 * 1024;
3.1.2 读取文件流并进行切片
切片可以用 file.slice()
里面传参就是 i
和 i + chunkSize
。切片后放到 formData
上传到后台。在这过程中可以处理进度条和记录当前切片是否上传成功。
// 记录文件切片是否上传成功
const statusArray = [];
// 获取当前文件大小
const curFileSize = file.size;
// 记录正在上传的切片的切片编号。这跟后台约定是 1 到 10,000 之间的正整数。
let num = 1;
for (let i = 0; i < curFileSize; i += chunkSize) {
// 切片初始上传状态为 false
statusArray[num - 1] = false;
const chunk = file.slice(i, i + chunkSize);
const fd = new FormData();
fd.append("file", chunk);
// 发起请求,将切片上传的后台
await this.chunkUpload(uploadId, fd, num, statusArray);
this.updateProgress(num / Math.ceil(curFileSize / chunkSize), file);
num += 1;
// 上传完成
if (
i + chunkSize >= curFileSize &&
statusArray.every((item) => item === true)
) {
this.completeUpload(uploadId, file, key);
}
}
3.2 切片上传
切片上传需要一个标识来区分当前分片的文件,这里用的 uploadId
,然后用 num
记录分片编号,statusArray
里面的布尔值确定切片的上传状态。下面是实例代码:
export default {
methods: {
async uploadAudio(file) {
// ...
await this.chunkUpload(uploadId, fd, num, statusArray);
// ...
},
async chunkUpload(uploadId, fd, num, statusArray) {
try {
const { data } = await axios.post(
`/api/videos/upload-part?upload_id=${uploadId}&part_no=${num}`,
fd,
{ headers: { "Content-Type": "multipart/form-data" } }
);
if (data.code === 0) {
statusArray[num - 1] = true;
}
} catch (error) {
console.error(error);
}
},
},
};
3.3 更新上传进度
可以留意之前用的 num / Math.ceil(curFileSize / chunkSize)
记录上传的进度,为什么要这么算?其实 Math.ceil(curFileSize / chunkSize)
就是算的当前文件一共会被切片的总数。然后用 num
去除以总数得到的就是百分比。下面是更新上传进度的具体代码:
export default {
methods: {
async uploadAudio(file) {
// ...
this.updateProgress(num / Math.ceil(curFileSize / chunkSize), file);
// ...
},
updateProgress(value, file) {
if (value <= 1) {
// 上传中
const percent = Number(value * 100).toFixed(1);
// 更新到 UI 或者记录到控制台的日志
// console.log("🚀 -> updateProgress -> percent:", percent);
}
},
},
};
3.4 完成上传
这里的实例是后台的存储框架自动完成的文件校验和合并。所以没有描述文件流合并的部分。否则可能有些细节要注意:
- 切片的管理:在切片生成和传输过程中,需要对切片进行管理。可以使用索引或 ID 来标识每个切片,以确保切片的正确顺序和完整性。
- 切片的组装:在接收端,需要将切片重新组装成完整的文件。这可以通过将切片按序合并或使用索引进行排序来实现。
- 错误处理和恢复:在切片传输过程中,可能会出现网络中断、传输错误或其他异常情况。需要实现适当的错误处理和恢复机制,例如重新传输丢失的切片或从上次中断的位置恢复传输。
- 安全性:对于包含敏感数据的文件,可能需要在切片生成时进行加密,并在传输和接收端进行解密操作,以确保数据的安全性。
这里 for
循环有一个判断 i + chunkSize >= curFileSize && statusArray.every((item) => item === true)
。这里就是当切片完成,并且每一个切片都上传成功的情况下发起请求。这样保证了切片的完整性,告诉后台文件保存已经准备好了。下面是实例代码:
export default {
methods: {
async uploadAudio(file) {
// ...
if (
i + chunkSize >= curFileSize &&
statusArray.every((item) => item === true)
) {
this.completeUpload(uploadId, file, key);
}
// ...
},
async completeUpload(uploadId, file, path) {
try {
const { data } = await axios.get(
`/api/videos/complete-upload/${uploadId}`
);
if (data.code === 0) {
this.saveFile(file, path);
}
} catch (error) {
console.error(error);
}
},
async saveFile(file, path) {
const { name, size } = file;
try {
const { data } = await axios.post("/api/videos/store", {
filename: name,
size,
path,
});
if (data.code === 0) {
// 上传成功,并且保存到了后台
} else {
// 上传失败
}
} catch (error) {
console.error(error);
}
},
},
};
3.5 小结
- 定义合适的切片大小,比如
1MB
。根据视频文件大小确定总的切片次数 for
循环中设置切片初始上传状态为false
,利用file.slice()
切片- 发请求将当前切片上传到后台,并且更新上传状态
- 更新当前视频文件上传进度条
- 条件满足所有切片上传完成且成功的情况下,发请求通知后台完成上传,保存视频文件到服务端
四、视频播放
要使用 tcplayer
进行视频播放,您需要进行以下步骤:引入 tcplayer
库、创建视频容器、初始化 tcplayer
。其他第三方播放器也是大同小异,大家可以参考下。
4.1 引入 tcplayer 库
播放器 SDK
支持 cdn
和 npm
两种集成方式。我们项目是通过 npm
集成的。
首先安装 tcplayer 的 npm 包:
npm install tcplayer.js
导入 SDK 和样式文件:
import TCPlayer from "tcplayer.js";
import "tcplayer.js/dist/tcplayer.min.css";
4.2 创建视频容器
在需要展示播放器的页面位置加入播放器容器。
<video id="player-container-id"></video>
说明:
- 播放器容器必须为
<video>
标签。- 示例中的 player-container-id 为播放器容器的 ID,可自行设置。
4.3 初始化 tcplayer
页面初始化后,即可播放视频资源。调用播放器实例上的方法,将 URL 地址传入方法。
// player-container-id 为播放器容器 ID,必须与 html 中一致
const player = TCPlayer("player-container-id", {
sources: [{ src: "path/to/video" }],
licenseUrl: "license/url", // 参考准备工作部分,在视立方控制台申请 license 后可获得 licenseUrl
});
const playerUrl = "https://vjs.zencdn.net/v/oceans.mp4"; // 这个链接是网上的 mp4 实例
player.src(playerUrl); // url 播放地址
五、感谢
如果本文对你有帮助,就点赞、评论和收藏支持下吧!感谢阅读。