【全网最细】前端如何解决大视频播放卡顿问题

239 阅读11分钟

一. 技术栈

在现代前端解决大视频播放卡顿问题的方案中,主要用到以下几类技术和工具:

1. FFmpeg

一个强大的开源多媒体处理工具,用于处理、转换、录制、编辑和流式传输音视频文件。

应用场景

  1. 视频压缩与优化:减少文件体积用于网络传输。
  2. 流媒体切片:生成 HLS 或 DASH 文件,用于流媒体播放。
  3. 格式兼容性处理:解决不同设备或平台对音视频格式的兼容性问题。
  4. 内容创作与编辑:快速裁剪、拼接和转码视频。
  5. 直播与录屏:实时推流到直播服务器。

2. HLS

一种流媒体传输协议,由苹果公司开发,用于通过 HTTP 分段传输音视频数据。

应用场景

  1. 视频点播(VOD):将大文件分段传输,解决加载时间长的问题。
  2. 直播流媒体:实时分段传输,实现稳定、低延迟的直播体验。
  3. 自适应码率播放:为不同用户提供网络适配的播放清晰度,提升跨设备观看效果。

3. HLS.js

🙋 看到这里可能有同学想问了,HLS.js 和 HLS 有什么关系,它们之间不就是多了个 .js 吗?

HLS.js 是一个基于 JavaScript 的工具库,专为在不原生支持 HLS 协议的浏览器中解析并播放 HLS 流媒体而设计,从而扩展了 HLS 的适用范围。

简单来说,HLS.js 就是用来兼容 HLS 的前端使用场景的

功能HLSHLS.js
定义与标准流媒体协议,定义分段传输和播放规则前端工具库,解析 HLS 流并播放
工作层面服务端(视频分段、播放列表生成)客户端(播放列表解析、加载、播放视频流)
依赖环境所有支持 HLS 的播放器或设备非原生支持 HLS 的浏览器(如 Chrome)
典型应用场景视频切片和播放协议标准前端实现兼容的 HLS 播放

小结

通过使用上面的 FFmpegHLS 和前端播放器技术,我们可以实现将大视频切片并部署到网络中,从而解决大视频播放的卡顿问题,同时提升用户体验。

技术汇总表

技术名称用途工具/库
FFmpeg视频处理与切片FFmpeg CLI
HLS流媒体播放协议FFmpeg,.m3u8 文件,HLS.js
HLS.js前端流媒体播放器库HLS.js,支持 HLS 流播放

二. 使用步骤

假设我们现在有一个视频叫 video.mp4,大小为 622.9 MB,总时长为:35:20 秒

这时候可能就面临以下问题:

  1. 加载延迟:整个视频文件需要先下载到一定程度,才能开始播放。
  2. 网络波动:在带宽不足或网络不稳定的情况下,播放容易卡顿甚至中断。
  3. 无法动态适配:高清视频在低网速环境下无法切换到更低码率版本,导致播放体验极差。

接下来我们手把手地解决这个问题

2-1. 安装 FFmpeg

1. Windows 环境

步骤一:下载 FFmpeg
  1. 访问 FFmpeg 官网的下载页面:FFmpeg 官方下载

  2. 在 "Windows" 下选择 "Windows builds by BtbN" 或其他构建源,点击下载最新的 FFmpeg 版本。

  3. 下载 ZIP 文件并解压到一个你希望存放 FFmpeg 的文件夹(例如 C:\ffmpeg)。

步骤二:配置系统环境变量
  1. 打开 系统属性,点击 高级系统设置
  2. 系统属性 窗口,点击 环境变量
  3. 系统变量 部分,找到并选中 Path 变量,点击 编辑
  4. 在编辑框中,点击 新建,然后输入 FFmpeg 解压文件夹中的 bin 目录路径。例如,如果你解压到 C:\ffmpeg,则路径为 C:\ffmpeg\bin
  5. 点击 确定 保存更改。
步骤三:验证安装
  1. 打开命令提示符(cmd)。

  2. 输入以下命令来验证 FFmpeg 是否安装成功:

    ffmpeg -version
    

    如果输出 FFmpeg 版本信息,则表示安装成功。


2. macOS 环境

步骤一:通过 Homebrew 安装 FFmpeg
  1. 首先确保你已经安装了 Homebrew。如果没有安装 Homebrew,可以通过以下命令进行安装:

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    
  2. 使用 Homebrew 安装 FFmpeg:

    brew install ffmpeg
    
  3. 安装完成后,FFmpeg 将自动添加到系统路径中,无需手动配置环境变量。

步骤二:验证安装
  1. 打开终端(Terminal)。

  2. 输入以下命令来验证 FFmpeg 是否安装成功:

    ffmpeg -version
    

    如果输出 FFmpeg 版本信息,则表示安装成功。


2-2. 使用 FFmpeg 进行视频切片

安装好 FFmpeg 后,我们就可以开始使用 FFmpeg 命令进行视频处理,将一个大视频切片为多个小片段以支持流媒体播放。

假设我们现在有一个视频文件 video.mp4,大小为 622.9 MB,总时长为 35:20 秒,我们希望将其切割成多个 10 秒的小片段,并生成一个 .m3u8 播放列表。

运行切片命令

ffmpeg -i video.mp4 \
       -c:v libx264 -c:a aac -f hls -hls_time 10 -hls_list_size 0 \
       -hls_segment_filename "output/part%03d.ts" \
       output/playlist.m3u8

命令说明

  • -i video.mp4:输入视频文件。
  • -c:v libx264:使用 H.264 编解码器对视频进行压缩。
  • -c:a aac:使用 AAC 编解码器对音频进行压缩。
  • -f hls:设置输出格式为 HLS,生成 .m3u8 播放列表和 .ts 片段。
  • -hls_time 10:指定每个 .ts 片段的时长为 10 秒。
  • -hls_list_size 0:指定播放列表的大小为 0,意味着播放列表可以无限增长,直到视频播放完成。
  • -hls_segment_filename "output/part%03d.ts":指定切片文件的命名规则,%03d 表示按照 3 位数字进行编号(例如 part000.ts, part001.ts)。
  • output/playlist.m3u8:指定生成的 .m3u8 播放列表文件。

执行结果

  • part000.ts, part001.ts, part002.ts...:这是切割后的视频片段。
  • playlist.m3u8:播放列表文件,浏览器或视频播放器根据它加载相应的 .ts 文件进行播放。

image.png

这样我们的视频文件就被切割成了多个小片段,这些片段可以独立地进行加载,从而避免了大视频文件的缓冲问题,有效地提高了流媒体播放的稳定性和响应速度。而接下来,播放器会根据生成的 .m3u8 播放列表动态加载并播放这些片段。

2-3. HLS.js 播放视频片段

我们已经通过 FFmpeg 将视频切片为多个 .ts 文件,并生成了一个 .m3u8 播放列表。接下来,我们需要在浏览器或播放器中播放这些切片,确保视频的流畅播放。所以我们需要使用 HLS.js 来实现。


1. 使用步骤

以下用原生 HTML 进行示例,各位同学可根据自己需求转 Vue 或者 React (都支持)

步骤 1: 引入 HLS.js

通过 CDN 引入:

<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>

通过 NPM 安装:

npm install hls.js
步骤 2: 播放视频

我们需要一个 HTML5 <video> 标签来播放视频。
然后,在 JavaScript 中初始化 HLS.js,并将其绑定到视频标签上。

<video id="video" width="100%" controls></video>

<script>
  if (Hls.isSupported()) {
    var video = document.getElementById('video');
    var hls = new Hls();

    // 绑定 HLS 流,加载播放列表
    hls.loadSource('path/to/your/playlist.m3u8');
    hls.attachMedia(video);

    // 监听 HLS.js 播放状态变化
    hls.on(Hls.Events.MANIFEST_PARSED, function () {
      console.log("HLS manifest loaded and parsed");
    });

    hls.on(Hls.Events.ERROR, function (event, data) {
      if (data.fatal) {
        switch (data.error) {
          case Hls.ErrorTypes.NETWORK_ERROR:
            console.error("Network error!");
            break;
          case Hls.ErrorTypes.MEDIA_ERROR:
            console.error("Media error!");
            break;
          case Hls.ErrorTypes.OTHER_ERROR:
            console.error("An unknown error occurred!");
            break;
          default:
            console.error("Fatal error:", data);
            break;
        }
      }
    });
  } else {
    console.error("HLS is not supported in this browser");
  }
</script>

代码解释

  • Hls.isSupported() :检查浏览器是否支持 HLS.js。
  • hls.loadSource('path/to/your/playlist.m3u8') :加载 .m3u8 播放列表文件,这个文件会告诉 HLS.js 如何加载视频的不同片段。
  • hls.attachMedia(video) :将 HLS.js 实例与 <video> 元素绑定,确保视频播放。
  • 事件监听:我们监听了 MANIFEST_PARSEDERROR 事件,分别用于确认播放列表已经解析并处理错误。


2. 动态自适应码率

HLS 协议的一个重要特性是 自适应码率流,也就是它可以根据网络带宽的变化,播放器可以自动选择更合适的码率播放视频,避免了由于带宽不足导致的视频卡顿。HLS.js 也支持这个功能。

  1. 不同码率的播放列表:我们可以使用 FFmpeg 生成多个不同分辨率和码率的视频文件,并创建多个 .m3u8 播放列表。

    比如:

    • playlist_1080p.m3u8:适用于高速网络,包含高质量的视频片段。
    • playlist_720p.m3u8:适用于中等带宽。
    • playlist_480p.m3u8:适用于低速网络。
  2. HLS.js 会根据网络状况自动切换:当带宽充足时,播放器会选择更高质量的流;当带宽不足时,它会自动降级到更低质量的流。


3. HLS.js 错误处理和调试

在播放过程中,可能会遇到网络问题、视频片段丢失等错误。HLS.js 提供了详细的错误回调机制,帮助我们捕捉和处理这些问题。

例如:

hls.on(Hls.Events.ERROR, function (event, data) {
  if (data.fatal) {
    switch (data.error) {
      case Hls.ErrorTypes.NETWORK_ERROR:
        console.error("Network error!");
        break;
      case Hls.ErrorTypes.MEDIA_ERROR:
        console.error("Media error!");
        break;
      case Hls.ErrorTypes.OTHER_ERROR:
        console.error("An unknown error occurred!");
        break;
      default:
        console.error("Fatal error:", data);
        break;
    }
  }
});

4. 事件类型

  • Hls.Events.ERROR:发生错误时触发。你可以根据 data.error 判断错误类型(如网络错误、媒体错误等),并进行相应处理。
  • Hls.Events.MANIFEST_PARSED:当 .m3u8 播放列表解析完成时触发。
  • Hls.Events.FRAG_LOADED:当某个视频片段加载完成时触发。
  • Hls.Events.LEVEL_SWITCHED:当切换到不同的码率(视频质量)时触发。

三. 进阶实现

上面我们实现了基本的 FFmpeg + HLS.js 播放大视频的功能,接下来我们做一个小小的进阶:

根据用户点击的节点,跳转到视频中的指定时间片段。

实现目标

  • 使用 HLS.js 播放 .m3u8 格式的视频。
  • 根据用户点击的节点,自动跳转到视频中的指定时间。
  • 高亮显示当前播放到的视频节点,帮助用户直观地了解播放进度。

HTML 页面结构

我们来创建一个简单的 HTML 页面,包含一个视频播放器和多个跳转节点。每个跳转节点会对应视频中的一个时间片段,当用户点击时,视频会跳转到对应的时间点并播放。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>视频节点播放功能</title>
  <style>
    body {
      margin: 0;
      background: #2A4897;
      display: flex;
      flex-direction: column;
      overflow-y: hidden;
    }

    video {
      width: 100%;
    }

    #nodeList {
      padding: 12px;
      padding-top: 36px;
      padding-bottom: 24px;
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      justify-content: space-between;
      overflow-y: auto;
      height: calc(100vh - 72.25vw);
      border-top: 1px solid #C0D8FD;
    }

    #nodeList button {
      padding-left: 58px;
      position: relative;
      display: flex;
      align-items: center;
      min-height: 50px;
      font-size: 14px;
      cursor: pointer;
      margin-bottom: 24px;
      padding-right: 12px;
      border-radius: 25px;
      color: #2A4897;
      border: 1px solid #C0D8FD;
      background: #fff;
    }

    .nodeList-num {
      position: absolute;
      left: 0;
      width: 38px;
      height: 38px;
      display: flex;
      justify-content: center;
      align-items: center;
      border-radius: 50%;
      background: #2A4897;
      border: 5px solid #C0D8FD;
      margin-right: 8px;
      color: #fff;
      font-size: 12px;
    }

    .active {
      background-color: #C0D8FD;
    }
  </style>
</head>
<body>
  <video id="myVideo" controls preload="auto">
    <source src="" type="application/x-mpegURL">
    Your browser does not support the video tag.
  </video>

  <div id="nodeList">
    <button data-time="27">
      <div class="nodeList-num"></div>
      <div>节点一</div>
    </button>
    <button data-time="112">
      <div class="nodeList-num"></div>
      <div>节点二</div>
    </button>
    <!-- 更多视频节点按钮 -->
  </div>

 <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
  <script>
    const video = document.getElementById("myVideo");
    const nodeList = document.getElementById("nodeList");
    const buttons = nodeList.querySelectorAll("button");

    // HLS.js 播放器初始化
    let hls;
    if (Hls.isSupported()) {
      hls = new Hls();
      hls.loadSource("/video/playlist.m3u8"); // 加载 m3u8 播放列表
      hls.attachMedia(video);
    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      video.src = '/video/playlist.m3u8'; // Safari 默认支持 m3u8
    }

    // 根据时间跳转到指定的时间片段
    const jumpToTime = (time) => {
      video.currentTime = time; // 设置视频的当前时间为指定的时间点
      video.play(); // 播放视频
    };

    // 绑定点击事件
    buttons.forEach((button) => {
      button.addEventListener("click", () => {
        const time = parseFloat(button.getAttribute("data-time"));
        jumpToTime(time);  // 跳转到指定时间
      });
    });

    // 高亮当前节点
    const highlightNode = (currentTime) => {
      buttons.forEach((button) => {
        const nodeTime = parseFloat(button.getAttribute("data-time"));
        button.classList.toggle("active", currentTime >= nodeTime && currentTime < nodeTime + 5);  // 高亮 5 秒内的节点
      });
    };

    // 视频播放更新时触发高亮逻辑
    video.addEventListener("timeupdate", () => {
      highlightNode(video.currentTime);
    });
  </script>
</body>
</html>

代码解析

  1. 加载 HLS.js: 在代码中,我们首先检测浏览器是否支持 HLS.js。如果支持,我们就创建一个 Hls 实例,加载 .m3u8 播放列表并将其绑定到 <video> 元素。

    let hls;
    if (Hls.isSupported()) {
      hls = new Hls();
      hls.loadSource("/video/playlist.m3u8");
      hls.attachMedia(video);
    }
    
  2. Safari 支持: 如果浏览器本身支持 HLS(例如 Safari),我们直接设置视频源为 .m3u8 播放列表。

    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      video.src = 'https://www.mstarpackaging.com/video-html/video/playlist.m3u8';
    }
    

时间跳转功能

通过设置 video.currentTime,我们可以控制视频播放到指定时间。

const jumpToTime = (time) => {
  video.currentTime = time;  // 设置视频时间
  video.play();  // 播放视频
};

节点高亮显示

timeupdate 事件中,我们根据当前时间来判断视频是否播放到某个节点,并给该节点添加 active 样式进行高亮显示。

const highlightNode = (currentTime) => {
  buttons.forEach((button) => {
    const nodeTime = parseFloat(button.getAttribute("data-time"));
    button.classList.toggle("active", currentTime >= nodeTime && currentTime < nodeTime + 5);
  });
};

如果有更优秀的实现方式或问题,欢迎大家评论区补充和提出 🌈