功能

  • video.js内嵌 截图、录制功能 (图片、视频会下载到本地)
  • 自定义全屏
  • 播放hls、flv、mp4
  • 功能集合成Vue组件

参考

播放hls、flv、mp4

安装

// video.js
npm install video.js
// 播放hls
npm install  videojs-contrib-hls
// 播放flv
npm install videojs-flvjs-es6
npm install flv.js

引入

import "videojs-contrib-hls";
import "videojs-flvjs-es6";
import videojs from "video.js";
import video_zhCN from "video.js/dist/lang/zh-CN.json";
videojs.addLanguage("zh-CN", video_zhCN);

export default {
  props: {
    name: {
      type: String,
      default: "my-video",
    },
    // 视频地址
    videoUrl: {
      type: String,
      default: "",
    },
    //视频宽度
    videoWidth: {
      type: String,
      default: "100%",
    },
    //视频高度
    videoHeight: {
      type: String,
      default: "100%",
    },
  },
  data() {
    const options = 
      {
        // 封面图
        poster: '',
        language: "zh-CN",
        // 设置自动播放
        autoplay: true,
        // 设置了它为true,才可实现自动播放,同时视频也被静音 (Chrome66及以上版本,禁止音视频的自动播放)
        muted: true,
        // 预加载
        preload: "none",
        // 显示播放的控件
        controls: true,
        // 进度条
        liveui: true,
        notSupportedMessage: "此视频暂无法播放,请稍后再试", // 允许覆盖Video.js无法播放媒体源时显示的默认信息。
        // 播放速率
        playbackRates: [0.5, 1, 2, 4, 8, 16],
        controlBar: {
          // 画中画
          pictureInPictureToggle: false,
          // 当前时间和持续时间的分隔符
          timeDivider: true,
          // 显示持续时间
          durationDisplay: true,
          // 是否显示剩余时间功能
          remainingTimeDisplay: true,
          // 是否显示全屏按钮
          fullscreenToggle: true,
        },
    }
    return {
      options,
      // videoUrl 从无到有时会显示一会视频错误信息
      // 使用该字段判断 no-video 来避免显示错误信息
      hasVideoUrl: false,
      // 视频流类型
      videoTypeObj: {
        mp4: 'video/mp4',
        flv: 'video/x-flv',
        m3u8: 'application/x-mpegURL'
      }
    };
  },
  methods: {
    getVideo(nowPlayVideoUrl) {
      if (!nowPlayVideoUrl) this.hasVideoUrl = false;
      if (!this.player) {
        this.player = videojs(this.$refs.player, this.options);
      }
      this.player.src([
        {
          src: nowPlayVideoUrl,
          type: this.videoUrl
          ?
          this.videoTypeObj[this.videoUrl.split('.').slice(-1)]
          :
          ''
        },
      ]);
      setTimeout(() => {
        this.hasVideoUrl = !!nowPlayVideoUrl;
      }, 100);
    }
  },
  watch: {
    //监听视频地址、video的id值
    videoUrl: {
      deep: true,
      immediate: true,
      handler(val) {
        this.$nextTick(() => {
          this.getVideo(val);
        });
      },
    },
  },
  beforeDestroy() {
    // 组件销毁时,清除播放器
    if (this.player) this.player.dispose();
  },
};

html

  <div
    :style="{ width: videoWidth, height: videoHeight, position: 'relative' }"
    class="display"
  >
    <video
      style="width: 100%; height: 100%"
      class="video-js"
      ref="player"
      v-show="videoUrl"
    >
    </video>
    <!-- 视频路径为空时 -->
    <div
      v-show="!hasVideoUrl"
      :style="{
        height: videoWidth,
        width: videoHeight,
        position: 'absolute',
        left: 0,
        top: 0,
        zIndex: 2,
        background: '#000',
      }"
    >
      no video
    </div>
  </div>

引入组件(视频地址使用的时西瓜的测试视频)


import Player from './components/Player.vue'

export default {
  name: 'App',
  components: {
    Player
  },
  data: {
      // flv
      nowPlayVideoUrl: "//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/flv/xgplayer-demo-360p.flv",
      // mp4
      // nowPlayVideoUrl: "//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4",
      // hls
      // nowPlayVideoUrl: "//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/hls/xgplayer-demo.m3u8",

  }
...
}
<template>
    <div style="height:400px;width:600px">
      <Player :videoUrl="nowPlayVideoUrl"/>
    </div>
</template>

加入自定义全屏

在props中加入 name 、fullscreenType、fullscreenChange属性

  props:{
    ...
  // 当存在多个直播时,用于判断是哪个直播需要全屏
   name: {
      type: String,
      default: "my-video",
    },
    // 全屏类型
    fullscreenType: {
      type: String,
      default: "initial",
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ["none", "DIY", "initial"].indexOf(value) !== -1;
     },
    // 全屏状态改变 当fullscreenType为DIY时有效
    fullscreenChange: {
      type: Function,
      default: () => {},
    },
}

修改options

   const options= {
    ...
    controlBar: {
          // 画中画
          pictureInPictureToggle: false,
          // 当前时间和持续时间的分隔符
          timeDivider: true,
          // 显示持续时间
          durationDisplay: true,
          // 是否显示剩余时间功能
          remainingTimeDisplay: true,
          // 是否显示全屏按钮
          fullscreenToggle: this.fullscreenType === "initial" ? true : false,
        },
}     

修改getVideo方法

if (!nowPlayVideoUrl) this.hasVideoUrl = false;
      if (!this.player) {
        this.player = videojs(this.$refs.player, this.options);
        // 自定义全屏功能
        if (this.fullscreenType === "DIY") this.setDIYFullscreen();
      }
    ...
}

编写setDIYFullscreen方法

setDIYFullscreen() {
      const fullscreenButton = this.player.controlBar.addChild("button");
      fullscreenButton.controlText("全屏");
      fullscreenButton.addClass("vjs-fullscreen-control");
      const fullscreenButtonDom = fullscreenButton.el();
      fullscreenButtonDom.onclick = () => {
        this.isDIYFullscreen = !this.isDIYFullscreen;
        if (this.isDIYFullscreen) {
          this.player.addClass("vjs-fullscreen");
          fullscreenButton.controlText("退出全屏");
          console.log('全屏');
        } else {
          this.player.removeClass("vjs-fullscreen");
          fullscreenButton.controlText("全屏");
          console.log('退出全屏');
        }
        // 父组件回调
        this.fullscreenChange(this.isDIYFullscreen, this.name);
      };
    }

修改父组件引入

<template>
    <div style="height:400px;width:600px">
      <Player fullscreenType="DIY" :videoUrl="nowPlayVideoUrl"/>
    </div>
</template>

效果

自定义全屏

加入截图、录制功能

安装

// 录制所需插件
npm i recordrtc

创建video.js文件,将video.js相关引入、逻辑都放整合放在这里(自定义全屏除外)

import videojs from "video.js";
import video_zhCN from "video.js/dist/lang/zh-CN.json";
import RecordRTC from "recordrtc";
import "videojs-contrib-hls";
import "videojs-flvjs-es6";
// 截图图片
import cameraImg from '../assets/images/camera.png'
// 录像图片
import monitorImg from '../assets/images/monitor.png'
videojs.addLanguage("zh-CN", video_zhCN);

// 创建 截图、录像
var Component = videojs.getComponent("Component");
var CustomBar = videojs.extend(Component, {
    constructor: function (player, options) {},
    createEl: function () {
        return videojs.dom.createEl('div', {
            innerHTML: `这是一个自定义组件`
        })
    }
})

// 注册 截图、录像组件
videojs.registerComponent('CustomBar', CustomBar);

export default videojs

创建custom-video.css文件,video相关css放在这里

@import "video.js/dist/video-js.css";

更改组件引入,以及在options中加入customBar 

import videojs from '../utils/video'
import '../assets/css/custom-video.css'
....
options = {
    customBar: {}
    ...
}

效果

 修改CustomBar,界面显示

...
// 截图图片
import cameraImg from '../assets/images/camera.png'
// 录制图片
import monitorImg from '../assets/images/monitor.png'

var CustomBar = videojs.extend(Component, {
    ...
  createEl: function () {
    const divDom = videojs.dom.createEl('div', {
      className: 'vjs-custom-bar',
      innerHTML: `
        <div  class="vjs-custom-control-bar vjs-button ac">
          <img src="${cameraImg}" style="width:13px" />
          <span class="ml10">截图</span>
        </div>
        <div class="mt10 vjs-custom-control-bar ac" >
          <img src="${monitorImg}" style="width:13px" />
          <span class="ml10">录像</span>
        </div>
      `
    })
    return divDom
  }
}

修改custom-video.css

customBar位于右侧中间显示,鼠标活动以及悬浮在customBar显示customBar,不活动时隐藏

录制中时红点闪烁

@import "video.js/dist/video-js.css";
.vjs-custom-bar {
  position: absolute;
  color: #fff;
  right: 10px;
  transform: translateY(-50%);
  top: 50%;
}
.vjs-custom-bar:hover {
  opacity: 1 !important;
}
.vjs-custom-control-bar {
  padding: 10px;
  background: rgba(43, 51, 63, 0.7);
  border-radius: 5px;
  cursor: pointer;
}
/* 开始录制 闪烁 */
.record-procees {
  display: inline-block;
  height: 10px;
  width: 10px;
  background: red;
  border-radius: 8px;
  animation: blings 1s linear infinite;
}
.mt10 {
  margin-top: 10px;
}
.ml10 {
  margin-left: 10px;
}
.ac {
  display: flex;
  align-items: center;
}
@keyframes blings {
  0% {
    opacity: 0.1;
  }
  100% {
    opacity: 1;
  }
}
.video-js .vjs-custom-bar {
  color: white;
  /* font-size: 2em; */
  padding: .5em;
 
 }
 
 .vjs-has-started .vjs-custom-bar {
  /* display: flex; */
  visibility: visible;
  opacity: 1;
  transition: visibility 0.1s, opacity 0.1s;
 }
 /* 用户不活动时设计title bar自动隐藏 */
 .vjs-has-started.vjs-user-inactive.vjs-playing .vjs-custom-bar {
 
  visibility: visible;
 
  /*visibility: hidden;*/
 
  opacity: 0;
 
  transition: visibility 1s, opacity 1s;
 
 }
 .vjs-controls-disabled .vjs-custom-bar,
 .vjs-using-native-controls .vjs-custom-bar,
 .vjs-error .vjs-custom-bar {
  display: none !important;
 }
 .vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-custom-bar {
  opacity: 0;
  visibility: visible;
  /*visibility: hidden;*/
 }
 .vjs-has-started.vjs-no-flex .vjs-custom-bar {
  display: table;
 }

CustomBar加入隐藏逻辑

...
constructor: function (player, options) {
    // Equivalent of `super(this, arguments)`
    Component.apply(this, arguments);
    // 隐藏截图
    if (options.screenshot === false) this.hiddenEl(0)
    // 隐藏录像
    if (options.recorder === false) this.hiddenEl(1)
},
createEl: function () {
...
}
  hiddenEl (index) {
    const myDom = this.el().querySelectorAll('div')[index]
    myDom.setAttribute('style', 'display:none')
  }

当需要隐藏录像时,修改options中customBar属性

options = {
        // 截图、录制Bar
        customBar: {
          screenshot: true,
          recorder: false
        }
    ....
}

customBar加入截图、录制逻辑

...  
constructor: function (player, options) {
    // Equivalent of `super(this, arguments)`
    Component.apply(this, arguments);
    // player 实列
    this.player = player
    // 录像所需要的 canvas
    this.canvas = null
    // 录像实列
    this.recorder = null
    // 停止循环帧 需要用到的参数
    this.animationFrame = null
    // 录像状态 false 未录像 true 录像中
    this.isRecorder = false
    // 隐藏截图
    if (options.screenshot === false) this.hiddenEl(0)
    // 隐藏录像
    if (options.recorder === false) this.hiddenEl(1)
},
createEl: function () {
    const divDom = videojs.dom.createEl('div', {
      className: 'vjs-custom-bar',
      innerHTML: `
        <div  class="vjs-custom-control-bar vjs-button ac">
          <img src="${cameraImg}" style="width:13px" />
          <span class="ml10">截图</span>
        </div>
        <div class="mt10 vjs-custom-control-bar ac" >
          <img src="${monitorImg}" style="width:13px" />
          <span class="ml10">录像</span>
        </div>
      `
    })
    const [screenshotDom, recordDom] = divDom.querySelectorAll('div')
    screenshotDom.onclick = () => this.screenshotHandle()
    recordDom.onclick = () => this.recordHandle(recordDom)
    return divDom
  },
...

截图 screenshotHandle方法

  // 截图
  screenshotHandle() {
    const fileType = "png";
    // 找到需要截图的video标签
    // video 实列
    const video = this.player.el().querySelector('video')
    // const video = this.video;
    console.log(video, 'video');
    const canvas = document.createElement("canvas");
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    console.log(canvas, 'canvas')
    canvas
      .getContext("2d")
      .drawImage(video, 0, 0, canvas.width, canvas.height); // 图片大小和视频分辨率一致
    const strDataURL = canvas.toDataURL("image/" + fileType); // canvas中video中取一帧图片并转成dataURL
    let arr = strDataURL.split(","),
      mime = arr[0].match(/:(.*?);/)[1],
      bstr = atob(arr[1]),
      n = bstr.length,
      u8arr = new Uint8Array(n);
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
    }
    const blob = new Blob([u8arr], {
      type: mime,
    });
    const url = window.URL.createObjectURL(blob);
    this.downloadFile(url, "png");
  },

录制 recordHandle方法,

  // 录像
  recordHandle (recordDom) {
    this.isRecorder = !this.isRecorder
    if (this.isRecorder) {
      recordDom.innerHTML = `<i class="record-procees"></i><span class="ml10">结束</span>`
      if (!this.canvas) this.canvas = document.createElement("canvas");
      this.recorder = RecordRTC(this.canvas, {
        type: "canvas",
      });
      this.recorder.startRecording();
      this.drawMedia();
    } else {
      // recordDom.innerHTML = `<i class="el-icon-video-camera-solid"></i><span class="ml10">录像</span>`
      recordDom.innerHTML = `<img src="${monitorImg}" style="width:13px" /><span class="ml10">录像</span>`
      this.recorder.stopRecording(() => {
        const url = window.URL.createObjectURL(this.recorder.getBlob());
        this.downloadFile(url, "mp4");
        cancelAnimationFrame(this.animationFrame);
        this.canvas = null;
        this.animationFrame = null;
      });
    }
  },
  // 刷新canvas
  drawMedia() {
    const ctx = this.canvas.getContext("2d");
    // 找到需要截图的video标签
    const video = this.player.el().querySelector('video')
    this.canvas.setAttribute("width", video.videoWidth);
    this.canvas.setAttribute("height", video.videoHeight);
    ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
    // requestAnimationFrame 根据电脑显示帧数进行循环
    this.animationFrame = requestAnimationFrame(() => this.drawMedia());
  },

文件下载downloadFile

  // 下载
  downloadFile: function (blob, fileType) {
    const a = document.createElement("a");
    a.style.display = "none";
    a.href = blob;
    // const time = this.parseTime(new Date())
    const time = new Date().getTime();
    a.download = `${time}.${fileType}`;
    document.body.appendChild(a);
    a.click();
    setTimeout(function () {
      document.body.removeChild(a);
      window.URL.revokeObjectURL(blob);
    }, 1000);
  },

解决RecordRTC录制报错

 

 在public下index.html引入该文件

    <!-- 下载到本地引入 -->
    <script src="screenshot.js"></script>
    <!-- 官方路径引入 -->
    <!-- <script src="https://www.webrtc-experiment.com/screenshot.js"></script> -->

效果

截图和视频录制

其中使用到的图片

图片1
图片1
图片2
图片2

 

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐