前言:

  • PC项目中的经常会有视频播放功能,播放一些课程视频或者直播回放等;而 h5 的 video 标签支持的视频格式为 MP4、WebM、Ogg;而有些视频的格式为 m3u8,此时需要使用 video.js 插件进行处理。

  • m3u8 是一种基于HTTP Live Streaming文件视频格式(直播流),它主要是存放整个视频的基本信息和分片(Segment)组成。目前由Apple.inc率先提出的HLS协议在Mac的Safari上原生支持【苹果提出HLS协议】;故其本身不兼容浏览器(如谷歌、IE等)播放;而video.js支持多平台播放,而且其下插件videojs-contrib-hls可对m3u8进行兼容浏览器播放处理;即videojs配合videojs-contrib-hls.js可以实现调用flash播放器播放hls。

  • 安装 video.jsvideojs-contrib-hls 插件:支持 m3u8 格式、并且是 hls 流
    npm install video.js --save
    npm install videojs-contrib-hls --save

资料参考:
【Vue】使用 videojs 做 hls 直播流(使用 http-flv 进行低延时优化说明)遇到的问题及解决方案总结(销毁、反复加载视频流)

一、封装视频组件

功能:更换视频、切换播放
代码:

<template>
  <div :style="{ width: videoWidth, height: videoHeight }">
    <video
      :id="videoId"
      style="width: 100%; height: 100%"
      class="video-js"
    ></video>
  </div>
</template>

<script>
import videojs from "video.js";
import "videojs-contrib-hls";

export default {
  props: {
    //视频地址、video的id值
    vData: {
      type: Object,
      default: () => {
        return {
          hlsurl: "", //视频url地址
          cameraId: "", //id
        };
      },
    },
    //视频宽度
    videoWidth: {
      type: String,
      default: "100%",
    },
    //视频高度
    videoHeight: {
      type: String,
      default: "100%",
    },
  },
  data() {
    return {
      options: {
        autoplay: true, // 设置自动播放
        muted: true, // 设置了它为true,才可实现自动播放,同时视频也被静音 (Chrome66及以上版本,禁止音视频的自动播放)
        preload: "auto", // 预加载
        controls: true, // 显示播放的控件
      },
      player: null,
      videoId: "",
    };
  },
  methods: {
    getVideo(nowPlayVideoUrl, nowPlayVideoId) {
      this.player = videojs(nowPlayVideoId, this.options);
      // 关键代码, 动态设置src,才可实现换台操作
      //不动态设置依然也可以这样写
      this.player.src([
        {
          src: nowPlayVideoUrl,
          type: "application/x-mpegURL", // 告诉videojs,这是一个hls流
        },
      ]);
    },
  },
  watch: {
    //监听视频地址、video的id值
    vData: {
      deep: true,
      immediate: true,
      handler(val) {
        this.videoId = val.cameraId;
        this.$nextTick(() => {
          this.getVideo(val.hlsurl, val.cameraId);
        });
      },
    },
  },
  beforeDestroy() {
    //  组件销毁时,清除播放器
    if (this.player) {
      this.player.dispose(); // 该方法会重置videojs的内部状态并移除dom
    }
  },
};
</script>

<style lang="scss" scoped>
</style>

页面引用组件:

<template>
	<BaseVideo :vData="vData"></BaseVideo>
</template>
<script>
export default {
	data() {
    	return {
    	 	vData:{
    	 		hlsurl: "http://xxx.xx.xxx.xx:xxxx/live/cameraid/1000065%240/substream/1.m3u8", //视频url地址
          		cameraId: "fid0", //id
    	 	}
    	}
    }
}
</script>

注意:
(1)直接在video改变src属性视频是不会切换的,所以需要动态设置 src

this.player.src([
        {
          src: nowPlayVideoUrl,
          type: "application/x-mpegURL", // 告诉videojs,这是一个hls流
        },
]);

(2)当设置自动播放 autoplay: false 时,会出现下面这个播放按钮。此外,当设置 autoplay: true 时,这个按钮有时候会出现,所以可以将这个按钮隐藏掉。
在这里插入图片描述

<style lang="scss" scoped>
/deep/ .video-js .vjs-big-play-button{
  display: none;
}
</style>

二、布局切换(单张、九宫格、四宫格)、分页

在这里插入图片描述
效果图:

<template>
  <div class="wrap_div">
    <div class="row_div">
      <!-- 布局 1x1  2x2  3x3 -->
      <div
        class="layout_div"
        v-for="(len, ind) in layoutConfig"
        :key="ind"
        @click="changeLay(len.status)"
      >
        <div :class="{ active_i: curlayout === len.status }">
          <i :class="len.icon"></i>
        </div>
      </div>

      <!-- 分页 -->
      <div class="pa_d">
        <div class="i_a" @click="toPre">
          <i class="el-icon-caret-left"></i>
        </div>
        <div>
          <span class="green">{{ currentPage }}</span
          >/<span>{{ Math.ceil(testRealTimeData.length / PageSize) }}</span>
        </div>
        <div class="i_a" @click="toNext">
          <i class="el-icon-caret-right"></i>
        </div>
      </div>
    </div>

    <div class="radio_div">
      <div class="all">
        <div
          class="video_div"
          :class="layoutConfig[this.curlayout].video_div_class"
        >
          <div class="video-wrap video_data">
            <div
              class="video_con"
              :class="{
                nine_: curlayout === 3,
                four_: curlayout === 2,
                first_: curlayout === 1,
              }"
              v-for="(item, index) in testRealTimeData.slice(
                (this.currentPage - 1) * this.PageSize,
                this.currentPage * this.PageSize
              )"
              :key="index"
            >
              <BaseVideo :vData="item"></BaseVideo>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import BaseVideo from "../../../common/components/new_base_video3.vue";

export default {
  name: "RealVideo",
  components: {
    BaseVideo,
  },
  props: {
    // 视频监控-实时预览
    realTimeData: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      curlayout: 1, //布局类型
      currentPage: 1, //当前页
      PageSize: 1, //一页几个
      // 视频数据
      testRealTimeData: [
        {
          
            hlsurl:
              "http://xxx.xx.xxx.xx:7086/live/cameraid/1000065%240/substream/1.m3u8",
            cameraId: "rid0",
          
        },
        {
         
            hlsurl:
              "http://xxx.xx.xxx.xx:7086/live/cameraid/1000065%240/substream/1.m3u8",
            cameraId: "rid1",
        
        },
        {
         
            hlsurl:
              "http://xxx.xx.xxx.xx:7086/live/cameraid/1000065%242/substream/1.m3u8",
            cameraId: "rid2",
          
        },
        {
          
            hlsurl:
              "http://172.20.105.67:7086/live/cameraid/1000065%241/substream/1.m3u8",
            cameraId: "rid3",
          
        },
        {
          
            hlsurl:
              "http://xxx.xx.xxx.xx:7086/live/cameraid/1000065%240/substream/1.m3u8",
            cameraId: "rid4",
          
        },
        {
          
            hlsurl:
              "http://xxx.xx.xxx.xx:7086/live/cameraid/1000065%241/substream/1.m3u8",
            cameraId: "rid5",
          
        },
        {
          
            hlsurl:
              "http://xxx.xx.xxx.xx:7086/live/cameraid/1000065%242/substream/1.m3u8",
            cameraId: "rid6",
         
        },
      ],

      // 布局配置: curlayout 的值 1  2  3
      layoutConfig: {
        1: {
          name: "1x1",
          status: 1,
          icon: "iconfont icon-1x1",
          PageSize: 1, //一页1个
          video_div_class: "first_div",
        },
        2: {
          name: "2x2",
          status: 2,
          icon: "iconfont icon-2x2",
          PageSize: 4, //一页4个
          video_div_class: "four_div",
        },
        3: {
          name: "3x3",
          status: 3,
          icon: "iconfont icon-3x3",
          PageSize: 9, //一页9个
          video_div_class: "nine_div",
        },
      },
    };
  },
  methods: {
    // 切换布局 1x1 2x2 3x3
    changeLay(num) {
      this.curlayout = num;
    },
    // 上一页
    toPre() {
      if (this.currentPage > 1) {
        this.currentPage = this.currentPage - 1;
      }
    },
    // 下一页
    toNext() {
      let total = Math.ceil(this.testRealTimeData.length / this.PageSize);
      if (this.currentPage < total) {
        this.currentPage = this.currentPage + 1;
      }
    },
  },

  watch: {
    // 计算布局: curlayout 的值 1  2  3
    curlayout() {
      this.PageSize = this.layoutConfig[this.curlayout].PageSize; //一页有多少个
      this.currentPage = 1; //当前是第几页
    },
  },
  computed: {},
};
</script>
<style scoped lang="scss">
.wrap_div {
  color: #fff;
  margin-top: -28px;
  height: 90%;
}
.row_div {
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
  align-items: center;
  margin-bottom: 20px;
  height: 28px;
}

.layout_div {
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  align-items: center;
}
.layout_div div {
  margin-right: 12px;
  color: rgba(137, 209, 222, 0.3);
}
.layout_div div:hover {
  color: rgba(137, 209, 222, 1);
}
.active_i {
  color: rgba(137, 209, 222, 1) !important;
}
.radio_div {
  height: 96%;
}
.pa_d {
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  align-items: center;
  font-size: 14px;
}
.i_a {
  width: 20px;
  height: 20px;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  cursor: pointer;
}
.pa_d i {
  color: #2c596d;
}
.green {
  color: #89d1de;
}

// 视频样式: 布局 1x1  2x2  3x3
.all {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: flex-start;
  height: 99%;

  .video_div {
    width: 100%;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: flex-start;
    height: 96%;
    // position: absolute;
  }

  .video_data {
    width: 100%;
    height: 100%;
    min-height: 360px;
    display: flex;
    flex-wrap: wrap;
  }
  .video_con {
    margin-right: 1%;
    margin-bottom: 5px;
    position: relative;
    min-height: 150px;
  }

  .first_ {
    width: 100%;
    height: 100%;
  }
  .four_ {
    width: 49.2%;
    height: 48%;
  }
  .four_:nth-child(2n) {
    margin-right: 0 !important;
  }
  .nine_ {
    width: 32.4%;
    height: 32%;
  }
  .nine_:nth-child(3n) {
    margin-right: 0 !important;
  }
}
</style>

三、排查视频播放错误原因

The media could not be loaded, either because the server or network failed or because the format is not supported

点我查看解决方案原文链接

在网站中用 HTML5 自带的 video 标签或者 video.js 的等插件的 HTML5 模式引入视频后,有少数用户反应观看所有视频时均无法播放,显示如下错误
在这里插入图片描述
反复排查,视频资源没问题,调用方式也没问题。而且出问题的用户所占比例非常小,所在地区、宽带线路也无规律,而且均反映土豆、优酷等站的视频能正常播放。
远程方式查看用户浏览器控制台,网络选项中提示视频资源404,由于视频托管在阿里云oss服务器中,提交工单,阿里工程师也没说出所以然,真是奇了怪了,无语。
反正能想到的都排查了,问题依旧。

一次偶然的机会,一位用户反应说是他们公司的网络管理员禁掉了视频相关协议,导致了这个错误。恍然大悟,之前一直没注意一个细节,就是几乎所有用户都反应说在家正常(家庭线路和自己的路由器一般不会有协议限制),在公司就不能播放,用户说其他网站视频播放正常,当时只考虑用户网络线路的问题了,哪能想到是坑货网管禁视频协议没禁彻底!!!!
PS:如果发生上述情况,从网站开发者角度来讲,基本无解,除非将视频换成flash播放形式,可能会好一些。

总结:

在排查这个问题的时候可以按照以下步骤依次排除原因

1、核实video的url地址是否有误

2、核实用户上网地区(有部分地区可能有限制,比如新疆、西藏)

3、核实用户的上网环境,是家庭网络还是公司网络,如果是家庭网络,让用户重启路由器试试。如果是公司网络,让用户问下公司网管是否禁掉相关协议。

4、如无法确定所连网络是否禁止相关协议,可以建议用户用电脑连接手机热点后再尝试播放,如果连接手机热点后能播放,那一定是相关视频协议被禁掉了。

5、有部分朋友出现在安卓手机上可以正常播放,但苹果手机iOS系统上无法播放的情况,经排查,是视频的问题,换一个视频或将视频重新转码就可以了。

四、其他

1、视频组件实现方法:将 <video> 标签写在 javascript 中

这种方法缺点是:视频的尺寸是计算出来,或者直接写死,在需要自适应的场景中就不够用了
代码:

<template>
  <div class="videomore"></div>
</template>

<script>
import videojs from "video.js";
import "videojs-contrib-hls";

export default {
  props: {},
  data() {
    return {};
  },
  methods: {
    getVideo(url, currentSize) {
      let html = `<video id="myVideo" fill style=" width: 300px;height: 300px;" class="video-js warn-video vjs-default-skin vjs-big-play-centered" controls preload="auto" data-setup='{}'>  
                                                                            
                                                                        </video>`;
      let node = document.createRange().createContextualFragment(html);

      document.querySelector(".videomore").appendChild(node);

      var myVideo = videojs("myVideo", {
        bigPlayButton: true,
        textTrackDisplay: false,
        posterImage: false,
        errorDisplay: false,
        autoplay: false,
        sources: [
          {
            src: url, //视频地址
            type: "application/x-mpegURL",
          },
        ],
      });
    },
  },
  watch: {},
  created() {
    // 视频地址
    this.getVideo('https://cdn.letv-cdn.com/2018/12/05/JOCeEEUuoteFrjCg/playlist.m3u8')
  },
};
</script>

<style lang="scss" scoped>
//隐藏播放按钮
/deep/ .video-js .vjs-big-play-button {
  display: none;
}
</style>

2、分流(有其他思路的小伙伴可以交流下哦~)

一个页面中播放多个视频,此时有可能会出现卡顿,解决思路有:
(1)使用 iframe 直接嵌入该视频(注意:有的视频不支持这种形式播放)

<iframe name= "iFrame1 " width=100% height=450 src= "/live/cameraid/1000090%240/substream/1.m3u8" allowfullscreen  scrolling= "auto " frameborder= "0 "> </iframe>

(2)使用 iframe 嵌入一个静态页面,在静态页面中使用视频组件

vue中通过 iframe 方式加载本地的 vue 页面的三种方法:

  • 第一种方法:
    就是直接使用具体的页面地址:http://127.0.0.1:8080/Index
<iframe
  src="http://127.0.0.1:8080/Index"
  id="frames"
  frameborder="0"
  sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
  style="height: 100%; width: 100%"
></iframe>
  • 第二种方法
    跟第一种方法类似,就是将127改为了 localhost
<iframe
  src="http://localhost:8080/Index"
  id="frames"
  frameborder="0"
  sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
  style="height: 100%; width: 100%"
></iframe>

不过,注意一下,第一种和第二种方法只能在本地访问时生效,当打包部署到线上后,就会出现页面找不到的问题,不能部署那用处少了太多,这时要用第三种方法

  • 第三种方法
    第三种方法跟页面中用相对路径引用图片的方式差不多,就是:…/index,这里的路径的路由中的路径
<iframe
  src="../index"
  id="frames"
  frameborder="0"
  sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
  style="height: 100%; width: 100%"
></iframe>

// 或者是像这样的路径
<iframe
  src="../#/index"
  id="frames"
  frameborder="0"
  sandbox="allow-forms allow-scripts allow-same-origin allow-popups"
  style="height: 100%; width: 100%"
></iframe>

像第三种方法,就可以实现在打包部署在线上时,正确访问到相应的页面。

不过注意一下,当在本地打包成功后,在本地进行测试时,会出现嵌入的页面不是你想要的页面,而是自己项目的树形文件结构。

这是正常的,当部署到线上后,就是正常的页面啦

3、获取当前页面的地址url

  • 传参前边的URLwindow.location.origin(可用来动态获取接口)
    例如:“http://yaoxin.test.jidecai.com:10080”
  • 域名 + 端口window.location.host
    例如:“yaoxin.test.jidecai.com:10080”
  • 当前URLwindow.location.href
    例如:“http://yaoxin.test.jidecai.com:10080/h/rule/ranger.html”
  • 协议window.location.protocol
    例如:“http:”
  • 域名:window.location.hostname
    “yaoxin.test.jidecai.com”
  • 端口:window.location.port
    例如:“10080”
  • 路径部分:window.location.pathname
    例如:“/h/rule/ranger.html”

拿到地址,使用 decodeURLcomponent 转码

4、vue+websocket实现数据实时推送以及本地测试

https://blog.csdn.net/qq_33426324/article/details/104491306#comments_15678839

Logo

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

更多推荐