vue2基于video.js,v8.21.0自己设计一个视频播放器

刚开始在网上下了点视频教程想着用些电脑自带的播放器。后来级数太多了,操作不方便。就开始自己捣鼓了。

痛点是是视频教程本身带来的,不方便反复的找重点。过去了不好快退等。

就基于video.js,设计了各种功能,鼠标移到上方显示

播放、暂停、静音、1倍速、1.5倍速、2倍速、⏪5秒、⏩5秒、上一节、下一节

现在做了分享。

在vue2项目中建个 list.vue 文件

html 复制代码
<template>
  <div class="zoo-common-layout">

    <!-- 右侧的视频模块 -->
    <div class="zoo-common-left">
      <my-player v-if="cUrl" :video-src="cUrl" @changeIndex="changeActiveIndex"></my-player>
    </div>

    <!-- 右侧的视频模块 -->
    <div class="zoo-common-right" :class="{'show': isShow}">
      <div class="drag-mian">
        <div class="drag-title">
          <i class="el-icon el-icon-arrow-left" v-if="isShow == false" @click="isShow = true"></i>
          <i class="el-icon el-icon-arrow-right" v-if="isShow == true" @click="isShow = false"></i>
          <span v-if="isShow == true">
            列表
          </span>
        </div>

        <div class="drag-search-box" v-if="isShow == true">
          <el-input placeholder="输入关键字" v-model="filterText" suffix-icon="el-icon-search" clearable />
        </div>
        
        <el-scrollbar class="drag-tree-scrollbar" v-loading="treeLoading">
          <el-tree 
            v-show = "refreshTree && isShow == true" 
            ref="treeBox" 
            :data="treeData" 
            :props="defaultProps" 
            highlight-current
            :expand-on-click-node="false" 
            node-key="id" 
            @node-click="handleNodeClick" 
            class="JNPF-common-el-tree"
            :filter-node-method="filterNode"
          >

          <span
            class="custom-tree-node"
            slot-scope="{ data, node }"
            :title="data.fullName"
            :class="{'marquee': true}"
          >
            <i>{{ node.id }} </i>
            <span class="text" :title="data.fullName">{{ node.label }}</span>
          </span>
          </el-tree>

          <div v-if ="isShow == false">
            <span  v-for="(node, index) in treeData"  :key="node.id">
            <el-tag :class="{'animated-border-box': index == activeIndex }" @click="handleNodeClick(node)">{{ index + 1 }}</el-tag>
          </span>
          </div>
       

        </el-scrollbar>
      </div>
    </div>
  </div>
</template>

<script>
import list from './list-data'
import MyPlayer from './video006-c.vue'

export default {
  name: 'Video006Component',
  components: {
    MyPlayer
  },
  data() {
    return {
      files: list,
      active: list[0],
      activeIndex: 0,
      cUrl: null,
      isShow: true,
      treeLoading: false,
      filterText: "",
      parentId: null,
      organizeId: "",
      treeData: [],
      defaultProps: {
        // id:'id',
        children: "children",
        label: "fullName",
      },
      expands: true,
      // 默认选中的节点key值
      defaultCheckedKeys: [1],
      refreshTree: true,
      isFullscreen: false,
    }
  },
  watch: {
    active(val) {
      this.cUrl = './' + val.parentNames.replace(" ", "")
    },
    filterText(val) {
      this.$refs.treeBox.filter(val);
    },
  },
  mounted() {
    this.cUrl = './' + this.active.parentNames.replace(" ", "");
    this.treeData = this.files.map((item, index) => {
      return {
        ...item,
        id: index + 1,
        num: index + 1,
        fullName: item.name,
        children: null
      };
    });
    console.log('this.treeData', this.treeData);

    const node =  this.$refs.treeBox.getNode(1);
    if (node) {
      node.setChecked(true);
    }

  },
  methods: {

    changeActive(item, index) {
      this.active = { ...item };
      this.activeIndex = index;
    },

    changeActiveIndex(sign) {
      if (sign === 'prev') {
        if (this.activeIndex === 0) {
          this.activeIndex = this.files.length - 1;
        } else {
          this.activeIndex -= 1;
        }
      } else {
        if (this.activeIndex === this.files.length - 1) {
          this.activeIndex = 0;
        } else {
          this.activeIndex += 1;
        }
      }
      let item = this.files[this.activeIndex];
      this.active = { ...item }
    },

    handleNodeClick(data) {
      
      console.log(data);

      if (this.organizeId === data.num) return;
      this.organizeId = data.num;
      this.active = { ...data };
      this.activeIndex = data.num - 1;
      console.log('data', data);
      let id = data.id;
       // 清除所有选中的节点  is-current
      this.$refs.treeBox.setCheckedNodes([]);  // 清空所有选中的节点
      let node =  this.$refs.treeBox.getNode(id);
      console.log('node', node);
      if (node) {
        this.$refs.treeBox.setCurrentNode(node);
      }
    },

    filterNode(value, data) {
      if (!value) return true;
      return data.fullName.indexOf(value) !== -1;
    },

    screenfullchange(isFullscreen) {
      this.isFullscreen = isFullscreen;
      console.log('isFullscreen', isFullscreen);
    },

  },
}
</script>

<style lang="scss" scoped>
.zoo-common-layout {
  position: relative;
  height: 100%;
  width: 100%;
  background-color: skyblue;
  display: flex;
  padding: 5px;
}
.zoo-common-left {
  height: 100%;
  flex: 1;
  margin-right: 12px;
}
.zoo-common-right {
  width: 64px;
  height: 100%;
  border-left: 1px solid #FF00FF;
}
.zoo-common-right.show {
  width: 240px;
  height: 100%;
}
.drag-mian {
  height: 100%;
  width: 100%;
  background-color: #FFF;
  border-radius: 5px;
}
.drag-title {
  font-size: 14px;
  font-weight: bold;
  text-align: center;
  width: 100%;
  height: 40px;
  line-height: 40px;
  position: relative;
}
.drag-title i {
  font-size: 24px;
  font-weight: bold;
  text-align: center;
  color: #2926e9;
  cursor: pointer;
  position: absolute;
  left: 5px;
  top: 5px;
}
.drag-search-box {
  padding: 5px 12px;
}
.drag-tree-scrollbar {
  height: calc(100% - 80px);
  overflow-y: auto;
}
.relation-graph-main {
  width: calc(100% - 220px);
  height: calc(100vh - 20px);
  margin: 10px;
  margin-right: 200px;
}
.drag-mian .el-tag {
  width: 60px;
  text-align: center;
  margin: 3px auto;
  cursor: pointer;
}
.animated-border-box {
  font-size: 16px;
  background-color: rgb(80, 98, 150);
  background: url(../assets/images/media-player/btn-active.png) center no-repeat;
  background-size: 80px 80px;
  color: #FFF;
}

::v-deep .el-tree .el-tree-node__content{
  height: 32px;
}

::v-deep .el-tree .is-checked{
  background-color: skyblue;
  color: #FFF;
}

::v-deep .el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content {
    background-color: skyblue;
    color: #FFF;
}

.custom-tree-node{
  width: 200px;
  text-align: left;
}

/* 树节点文字的跑马灯效果 */
.marquee {
  display: inline-block;
  width: 200px; /* 设置容器的宽度 */
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  vertical-align: middle;
  position: relative;
}

.marquee .text {
  display: inline-block;
  white-space: nowrap;
  // animation: marquee 10s linear infinite;
  width: 160px; /* 设置容器的宽度 */
  overflow: hidden;
  white-space: nowrap;
}

/* 跑马灯动画 */
@keyframes marquee {
  0% {
    transform: translateX(100%);
  }
  100% {
    transform: translateX(-100%);
  }
}
</style>

然后 再建一个 video006-c.vue 作为视频播放组件。

html 复制代码
<template>
  <div class="zoo-w-wrapper">
    <div class="v-options">
      <div class="v-c-options">
        <!-- 控制按钮 -->
        <div class="option-btn" @click="videoPlay">播放</div>
        <div class="option-btn" @click="videoPause">暂停</div>
        <div class="option-btn" @click="videoMute">静音</div>
        <div class="option-btn" :class="{ 'active': speed === 1 }" @click="videoDoubleSpeed(1)">1倍速</div>
        <div class="option-btn" :class="{ 'active': speed === 1.5 }" @click="videoDoubleSpeed(1.5)">1.5倍速</div>
        <div class="option-btn" :class="{ 'active': speed === 2 }" @click="videoDoubleSpeed(2)">2倍速</div>

        <!-- 5秒跳跃 -->
        <div class="option-btn" @click="skipTime(-5)">⏪5秒</div>
        <div class="option-btn" @click="skipTime(5)">⏩5秒</div>
        
        <!-- 上一节、下一节 -->
        <div class="option-btn" @click="changeActiveIndex('prev')">上一节</div>
        <div class="option-btn" @click="changeActiveIndex('next')">下一节</div>
      </div>
    </div>
    <!-- 视频播放器 -->
    <video ref="videoPlayer" class="video-js vjs-default-skin" controls>
      <source :src="videoSrc" type="video/mp4" />
    </video>
  </div>
</template>

<script>
import videojs from 'video.js';
import 'video.js/dist/video-js.css';

export default {
  props: {
    videoSrc: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      player: null, // video.js 实例
      speed: 1, // 当前播放速度
    };
  },
  mounted() {
    // 初始化 video.js 播放器
    this.player = videojs(this.$refs.videoPlayer);

    // 监听播放器的播放事件来更新播放状态
    this.player.on('play', () => {
      console.log('Video is playing');
    });

    // 监听播放器的暂停事件
    this.player.on('pause', () => {
      console.log('Video is paused');
    });
  },
  watch: {
    // 监听 videoSrc 的变化
    videoSrc(newSrc) {
      if (this.player) {
        this.player.src({ type: 'video/mp4', src: newSrc });
        this.player.load(); // 强制加载新的源
        this.player.play(); // 自动播放新的源(可选)
        this.videoDoubleSpeed(2);
      }
    }
  },
  beforeDestroy() {
    // 清理 video.js 实例
    if (this.player) {
      this.player.dispose();
    }
  },
  methods: {
    // 播放视频
    videoPlay() {
      if (this.player) {
        this.player.play();
      }
    },

    // 暂停视频
    videoPause() {
      if (this.player) {
        this.player.pause();
      }
    },

    // 静音/取消静音
    videoMute() {
      if (this.player) {
        const isMuted = this.player.muted();
        this.player.muted(!isMuted); // 切换静音状态
      }
    },

    // 设置视频播放速度
    videoDoubleSpeed(newSpeed) {
      if (this.player) {
        this.speed = newSpeed;
        this.player.playbackRate(newSpeed);
      }
    },

    // 跳跃时间
    skipTime(seconds) {
      if (this.player) {
        const currentTime = this.player.currentTime();
        this.player.currentTime(currentTime + seconds);
      }
    },

    // 上一节/下一节事件(调用父组件的方法)
    changeActiveIndex(direction) {
      this.$emit('changeIndex', direction); // 向父组件传递事件
    }
  }
};
</script>

<style lang="scss" scoped>
.zoo-w-wrapper{
  width: 100%;
  height: 100%;
  position: relative;
}

.v-options{
  position: absolute;
  left: 0;
  top: 0;
  height: 96px;
  width: 100%;
  background-color: rgba($color: #24c46c, $alpha: .1);
  z-index: 99;
  cursor: pointer;
  .v-c-options{
    display: none;
    height: 60px;
    width: 100%;
    line-height: 60px;
    background: linear-gradient(to right, #3fbfdf, #650472, #146809, #2600ff);
    // background: #2600ff;
    position: relative;
    z-index: 999;
  }
}

.v-options:hover .v-c-options{
  display: flex;
}



.v-c-options{

   .option-btn {
      width: 88px;
      height: 32px !important;
      line-height: 32px;
      background: url(../assets/images/media-player/btn-def.png) center no-repeat;
      background-size: 100%;
      color: #fff;
      cursor: pointer;
      margin: 16px 5px 0;
      text-align: center;
      box-shadow: 0 0 1px 1px #b4e6ff;
    }

   .option-btn:hover {
      background-color: rgb(80, 98, 150);
      background: url(../assets/images/media-player/btn-active.png) center no-repeat;
      background-size: 100%;
    }

   .option-btn.active {
      width: 88px;
      height: 32px !important;
      line-height: 32px;
      background: url(../assets/images/media-player/btn-active.png) center no-repeat;
      background-size: 100%;
      color: #fff;
      cursor: pointer;
      margin: 16px 5px 0;
      text-align: center;
      box-shadow: 0 0 1px 1px #b4e6ff;
    }

      .option-btn.success {
        width: 150px;
        background: url(../assets/images/media-player/btn-green.png) center no-repeat;
        background-size: 100%;
        cursor: default;
        margin-right: 12px;
        margin-left: 12px;
      }

      .option-btn.success:hover {
        background: url(../assets/images/media-player/btn-green.png) center no-repeat;
        background-size: 100%;
        cursor: default;
      }

    .active-course {
      color: #FFF;
      flex-direction: row;
      display: flex;
      justify-content: flex-start;

      .success {
        width: 100px;
        background: url(../assets/images/media-player/btn-green.png) center no-repeat;
        background-size: 100%;
        cursor: default;
        margin-right: 12px;
        margin-left: 12px;
      }

      .success:hover {
        background: url(../assets/images/media-player/btn-green.png) center no-repeat;
        cursor: default;
      }
    }

  }


  .moveCotr.right,
  .moveCotr.left {
    width: 32px;
    height: 64px;
    border-radius: 100%;
    cursor: pointer;
    position: absolute;
    left: 0;
    top: 50%;
    margin-left: -32px;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: -32px;

    img {
      opacity: 0.6;
    }

    img:hover {
      opacity: 0.8;
    }
  }

.video-js{
  width: 100%;
  height: calc(100vh - 32px);
  margin-top: 12px;
}
</style>

另外有个 list-data.js 文件 作文存放 列表数据的

html 复制代码
const list = [
    {
      name: '.-S07-01.mp4',
      type: 'file',
      parentNames: 'wwd第7季25集/.-S07-01.mp4',
      realName: '.mp4'
    },
    {
      name: '.-S07-02.mp4',
      type: 'file',
      parentNames: 'wwd第7季25集/.-S07-02.mp4',
      realName: '.mp4'
    }
]

export default list;

这里的 资源放到 public 下

然后修改对应信息 就可以正常播放了。

另外会有些图片会报错,自己解决下哈。或者si我 。

祝顺利体验。

相关推荐
一个小猴子`3 小时前
FFMpeg视频编码实战和音频编码实战
ffmpeg·音视频
EasyDSS5 小时前
国标GB28181视频平台EasyCVR如何搭建汽车修理厂远程视频网络监控方案
网络·音视频
无证驾驶梁嗖嗖8 小时前
FFMPEG大文件视频分割传输教程,微信不支持1G文件以上
音视频
一个小猴子`9 小时前
FFMpeg音视频解码实战
ffmpeg·音视频
小白教程10 小时前
Python爬取视频的架构方案,Python视频爬取入门教程
python·架构·音视频·python爬虫·python视频爬虫·python爬取视频教程
Json____12 小时前
springboot 处理编码的格式为opus的音频数据解决方案【java8】
spring boot·后端·音视频·pcm·音频处理·解码器·opus
赤鸢QAQ12 小时前
ffpyplayer+Qt,制作一个视频播放器
python·qt·音视频
EasyNTS13 小时前
ONVIF/RTSP/RTMP协议EasyCVR视频汇聚平台RTMP协议配置全攻略 | 直播推流实战教程
大数据·网络·人工智能·音视频
少年的云河月17 小时前
OpenHarmony 5.0版本视频硬件编解码适配
音视频·harmonyos·视频编解码·openharmony·codec hdi