videojs增加视频源选择框小工具

一、前言

在使用 video.js 开发视频播放功能时,我们经常需要根据不同网络环境或视频源,提供"多清晰度切换"功能(例如:720P、1080P、4K 等)。

虽然 video.js 提供了插件机制,但它本身没有自带清晰度切换逻辑,因此我们可以 通过继承组件的方式自定义一个清晰度下拉按钮,实现动态切换视频源的能力。

本文将结合完整源码,讲解实现思路与关键细节。


二、核心思路

整个功能的实现分为三个主要部分:

  1. 初始化播放器 :创建 video.js 实例并配置视频源;

  2. 自定义组件 QualityDropdown :继承 video.js 的 Button,实现一个带下拉菜单的按钮;

  3. 事件与逻辑控制:处理点击、菜单显示/隐藏、视频源切换等逻辑。

三、源码

复制代码
const initVideo = () => {
  const url = result.value.videoUrl;

  // 初始化播放器
  player.value = videojs(
    videoPlayer.value,
    {
      controls: true,
      autoplay: false,
      preload: 'auto',
      fluid: false,
      responsive: true,
      width: '100%',
      height: '100%',
      sources: [{ src: url, type: getVideoType(url) }],
    },
    () => {
      const videoPlayerInstance = player.value;

      // ---------- 添加清晰度切换按钮 ----------
      const Button = videojs.getComponent('Button');

      class QualityDropdown extends Button {
        constructor(player, options) {
          super(player, options);
          this.controlText('清晰度');
          this.sources = options.sources || [];
          this.current = 0;

          this.el().classList.add('vjs-quality-button');
          this.el().innerText = this.sources[this.current]?.label || '清晰度';

          // 创建下拉菜单
          this.menu = document.createElement('div');
          this.menu.className = 'vjs-quality-menu';
          this.menu.style.display = 'none';
          this.menu.style.position = 'absolute';
          this.menu.style.background = 'rgba(0,0,0,0.8)';
          this.menu.style.color = '#fff';
          this.menu.style.padding = '5px 0';
          this.menu.style.borderRadius = '4px';
          this.menu.style.top = '-70px';
          this.menu.style.left = '0';
          this.menu.style.zIndex = '1000';
          this.el().appendChild(this.menu);

          this.sources.forEach((source, index) => {
            const item = document.createElement('div');
            item.innerText = source.label;
            item.style.padding = '5px 10px';
            item.style.cursor = 'pointer';
            item.onmouseenter = () => (item.style.background = 'rgba(255,255,255,0.2)');
            item.onmouseleave = () => (item.style.background = 'transparent');
            item.onclick = () => {
              this.current = index;
              this.player().src(source);
              this.player().play();
              this.el().firstChild.nodeValue = source.label;
              this.menu.style.display = 'none';
            };
            this.menu.appendChild(item);
          });

          // 点击按钮显示/隐藏菜单
          this.el().onclick = (e) => {
            e.stopPropagation();
            this.menu.style.display = this.menu.style.display === 'none' ? 'block' : 'none';
          };

          // 点击页面其他地方隐藏菜单
          document.addEventListener('click', () => {
            this.menu.style.display = 'none';
          });
        }
      }
      // ---------- 添加清晰度切换按钮 ----------
      videojs.registerComponent('QualityDropdown', QualityDropdown);

      const qualityBtn = videoPlayerInstance.addChild('QualityDropdown', { sources: result.value.videoSources });

      // 插入到控制条末尾(进度条后面)
      const controlBarChildren = videoPlayerInstance.controlBar.children();
      videoPlayerInstance.controlBar.addChild(qualityBtn, {}, controlBarChildren.length - 1);

      // ---------- 原有事件监听 ----------
      videoPlayerInstance.on('timeupdate', () => {
        const currentTimeInSeconds = Math.floor(videoPlayerInstance.currentTime());
        updatePositionByTime(currentTimeInSeconds);
        console.log(`当前时间: ${videoPlayerInstance.currentTime().toFixed(2)}秒 / 总时长: ${videoPlayerInstance.duration().toFixed(2)}秒`);
      });

      videoPlayerInstance.on('pause', () => console.log('视频已暂停'));
      videoPlayerInstance.on('seeking', () => console.log('正在跳转到新位置...'));
      videoPlayerInstance.on('seeked', () => console.log('跳转完成,当前时间:', videoPlayerInstance.currentTime()));
      videoPlayerInstance.on('loadedmetadata', () => {
        console.log('视频元数据加载完成');
        videoPlayerInstance.dimensions(videoPlayerInstance.width(), videoPlayerInstance.height());
      });

      window.addEventListener('resize', () => {
        videoPlayerInstance.dimensions(videoPlayerInstance.width(), videoPlayerInstance.height());
      });
    }
  );
};

四、关键实现解析

1️⃣ 继承 video.js 组件系统

video.js 的组件化体系允许我们继承基础组件(如 Button),从而快速扩展功能。

复制代码
const Button = videojs.getComponent('Button');
class QualityDropdown extends Button { ... }
videojs.registerComponent('QualityDropdown', QualityDropdown);

这段代码的核心在于:

  • 保留原有按钮行为;

  • 增加自定义下拉菜单;

  • 在 controlBar 中动态注册。

2️⃣ 动态生成菜单项

我们通过遍历 sources 数组,为每种清晰度创建一个 <div> 选项:
*

复制代码
  this.sources.forEach((source, index) => {
    const item = document.createElement('div');
    item.innerText = source.label;
    ...
  });

切换清晰度的核心逻辑

点击选项后,调用:
*

复制代码
  this.player().src(source);
  this.player().play();

重新设置视频源,并立即播放。

由于 video.js 会自动重新加载元数据,这种切换方式简单直接,兼容性很好。

相关推荐
归秋1422 分钟前
多轨音频智能混音软件怎么选:从 demo 到更完整作品的 AI 后期工具思路
人工智能·音视频
用户91775337187526 分钟前
从零封装一个地图组件库:OpenLayers + Vue 的工程化实践
vue.js
樱花落木兰34 分钟前
分布式登录实战:Session 会话共享改造,Redis 存储用户登录状态
java·javascript·数据库·redis·分布式·缓存
liangshanbo12151 小时前
面试题:线上 JavaScript 报错如何快速定位到源码?
开发语言·javascript·ecmascript
雪芽蓝域zzs2 小时前
第7节:多选模式、自定义选项渲染、受控弹窗显隐
vue.js·elementui
小猴子爱上树2 小时前
跨马翻译:批量图片翻译与视频字幕、智能抠图一站式在线图片翻译工具
大数据·人工智能·python·音视频
每天死循环3 小时前
ComfyUI MiniMax H3文生视频(t2v)提示词教程:四段式写法+5秒768p实测(附模板和参数)
音视频·文生视频·comfyui·ai视频·minimax
专业程序开发源3 小时前
django新闻推荐系统70655-计算机课程设计、毕业设计
java·javascript·spring boot·后端·python·django·课程设计
acd120093 小时前
前端随笔:数据明明变了,Vue 页面就是不更新
前端·javascript·vue.js
vx_Biye_Design3 小时前
springboot一站式旅游管理平台81037-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·课程设计·express·旅游