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

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

一、前言

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

  • 开发背景: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 小结

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

三、大视频分片上传

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

先简单看看效果:

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

3.1 文件流切片

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

3.1.1 定义切片大小

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

// 切片大小(1MB)
const chunkSize = 1024 * 1024;

3.1.2 读取文件流并进行切片

切片可以用 file.slice() 里面传参就是 ii + 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 小结

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

四、视频播放

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

4.1 引入 tcplayer 库

播放器 SDK 支持 cdnnpm 两种集成方式。我们项目是通过 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 播放地址

原文链接:https://juejin.cn/post/7320231946009395212 作者:gyx_这个杀手不太冷静

(0)
上一篇 2024年1月6日 上午10:32
下一篇 2024年1月6日 上午10:43

相关推荐

发表回复

登录后才能评论