极限拆解:大视频分片上传与播放的完美解决方案

极限拆解:大视频分片上传与播放的完美解决方案

一、前言

在当今数字时代,视频内容的传输播放 需求不断增长。本文将介绍一种创新的解决方案 ,以实现大视频分片上传和流畅播放。并会提供参考代码,帮助读者深入理解和实践。

  • 开发背景:Web 端需要支持上传视频文件,大小限制最大为 1G。上传后播放器可以直接播放。
  • 开发框架、库:
    • Vue 2
    • Axios
    • TCPlayer
  • 难点描述:
    • 大视频分片上传
    • 视频播放

二、普通视频直传

在前端实现视频上传的过程中,通常会使用 HTML5 中的 File API 和网络请求来完成文件的选择和上传。一般情况下,视频大小在 100M 以内可以考虑直接上传,不需要分片。

2.1 HTML 部分

HTML 中大多数情况会隐藏原生 Input 标签,然后自定义样式。用自己自定义的按钮点击模拟 Input 的点击出发 change 事件。

html 复制代码
<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 的部分,主要是完成了文件的过滤和上传。

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 小结

  1. 定义一个文件选择输入框 input type="file"和一个触发上传的按钮div
  2. 获取文件选择输入框中选中的视频文件 e.target.files
  3. 过滤掉大小和格式不符合要求的文件
  4. 创建 formData,并且设置 Content-Type 后发起上传请求

三、大视频分片上传

大视频主要指 1G 左右,甚至更大的视频。太大了,一般文件直传接口会超时,导致上传失败。通常会考虑将文件分片后上传,并做进度监控。

先简单看看效果:

3.1 文件流切片

文件流切片是一种将文件分割成较小的片段或块的技术。它在文件上传、大文件处理以及网络传输等场景中非常有用。通过将文件切片成较小的块,可以更高效地处理和传输文件数据。

3.1.1 定义切片大小

这里文件流存储以"字节"(Byte)为单位。那么 1MB 就是 1024 * 1024

js 复制代码
// 切片大小(1MB)
const chunkSize = 1024 * 1024;

3.1.2 读取文件流并进行切片

切片可以用 file.slice() 里面传参就是 ii + chunkSize。切片后放到 formData 上传到后台。在这过程中可以处理进度条和记录当前切片是否上传成功。

js 复制代码
// 记录文件切片是否上传成功
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 里面的布尔值确定切片的上传状态。下面是实例代码:

js 复制代码
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 去除以总数得到的就是百分比。下面是更新上传进度的具体代码:

js 复制代码
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)。这里就是当切片完成,并且每一个切片都上传成功的情况下发起请求。这样保证了切片的完整性,告诉后台文件保存已经准备好了。下面是实例代码:

js 复制代码
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 小结

  1. 定义合适的切片大小,比如 1MB。根据视频文件大小确定总的切片次数
  2. for 循环中设置切片初始上传状态为 false,利用 file.slice() 切片
  3. 发请求将当前切片上传到后台,并且更新上传状态
  4. 更新当前视频文件上传进度条
  5. 条件满足所有切片上传完成且成功的情况下,发请求通知后台完成上传,保存视频文件到服务端

四、视频播放

要使用 tcplayer 进行视频播放,您需要进行以下步骤:引入 tcplayer 库、创建视频容器、初始化 tcplayer。其他第三方播放器也是大同小异,大家可以参考下。

4.1 引入 tcplayer 库

播放器 SDK 支持 cdnnpm 两种集成方式。我们项目是通过 npm 集成的。

首先安装 tcplayer 的 npm 包:

shell 复制代码
npm install tcplayer.js

导入 SDK 和样式文件:

js 复制代码
import TCPlayer from "tcplayer.js";
import "tcplayer.js/dist/tcplayer.min.css";

4.2 创建视频容器

在需要展示播放器的页面位置加入播放器容器。

html 复制代码
<video id="player-container-id"></video>

说明:

  • 播放器容器必须为 <video> 标签。
  • 示例中的 player-container-id 为播放器容器的 ID,可自行设置。

4.3 初始化 tcplayer

页面初始化后,即可播放视频资源。调用播放器实例上的方法,将 URL 地址传入方法。

js 复制代码
// 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 播放地址
相关推荐
xjt_090113 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农25 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发2 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法