近些时间,在项目开发过程中,遇到与视频相关的内容,即前端播放直播流rtmp/rtsp/hls等。而针对这个问题,我们则需要使用videojs这个插件来进行开发,当然如果是使用了vue-cli的话,可以采用vue-video-player,这是对videojs做了进一步的封装,可以引入做组件使用。详细过程如下:

# 1. VIDEOJS: ERROR: The "flash" tech is undefined. Skipped browser support check for that tech

    顾名思义,就是你的插件里的flash找不到了,具体原因在排查过程中觉得可能是内外都有videojs-flash造成的。这个问题也很经典,网上解决方法五花八门,但真正能用的好像都没有。
    针对这个问题,首先要确保所有相关的插件都是使用npm安装而不是cnpm安装,因为 cnpm 安装 的插件可能会有奇奇怪怪的问题。确保了这个之后,如果是使用了 vue-video-player 来做开发的话,需要进行以下步骤:

  • 1. vue-video-player本身已内置了videojs-flash,以及video-contrib-hls这两个分别播放rtmp / m3u8的插件,所以安装 vue-video-player无需再另外安装这两个插件
  • 2. 除去第一步,在排除没有禁用网站flash的情况下,如果还报这个错误的话,可参考把node_modules,package-lock.json一起移除掉后,再 npm install
  • 3. 第二步尝试 1次之后如果无效,目前我的解决方法是移除掉所有跟videojs有关的插件,直接npm i vue-video-player@5.0.2 -S这个问题,目前我就是这样就解决了,具体原因推测为videojs-flash互相冲突,网上很多种方法都不一定有用,遇到这个问题一定要耐心多尝试几次,也许就能搞定了


# 2. 封装视频播放组件

    以下只是其中一个例子,主要用于播放rtmp流的直播视频,需要浏览器开启flash配合使用,具体代码如下:

<!--该组件除了默认的底部控制条外,监听了播放,暂停,结束事件,以及设置了顶部条的最大化和关闭视频-->
<template>
  <!--双击全屏,默认就是这样,这一步主要针对videojs没有配置控制条实现-->
  <div
    @dblclick.stop="onVideoDBClick"
    class="rmtp-video-player"
    @mousemove="showTitleBar = true"
    @mouseleave="showTitleBar = false"
  >
    <div class="head-menu" :class="{showbar: showTitleBar}">
      <slot name="video-name"></slot>
      <div class="right-button">
        <!--这里阻止点击事件冒泡-->
        <i
          class="icon-maximize"
          @click.prevent="onMaximize"
          :title="Maximize ? '还原' : '最大化'"
          :class="Maximize ? 'el-icon-files' : 'el-icon-full-screen'"
        />
        <i class="el-icon-close" @click.prevent="onClose" title="关闭" />
      </div>
    </div>
    <video-player
      class="video-player vjs-custom-skin"
      ref="videoPlayer"
      :playsinline="true"
      :options="videoParams"
      @play="onPlayerPlay"
      @pause="onPlayerPause"
      @ended="onPlayerEnded"
    ></video-player>
  </div>
</template>

<script>
  import 'video.js/dist/video-js.css';
  import 'videojs-flash';
  import 'videojs-contrib-hls';
  import {videoPlayer} from 'vue-video-player';
  export default {
    props: {
      videoOptions: [Object], //视频播放选项
      videoUrl: {
        //视频地址
        type: String,
        default: '',
      },
      videoPoster: {
        //视频封面
        type: String,
        default: '',
      },
      maximize: {
        // 是否开启了最大化
        type: Boolean,
        default: false,
      },
    },
    components: {
      // 引入videoplayer
      videoPlayer,
    },
    computed: {
      videoParams: {
        get() {
          return {
            playbackRates: [0.7, 1.0, 1.5, 2.0],
            // autoplay: true,
            muted: false,
            // controls: false,
            loop: true,
            preload: 'auto',
            language: 'zh-CN',
            fluid: false,
            sources: [
              {
                withCredentials: false,
                type: '',
                src: this.videoUrl,
              },
            ],
            flash: {
              hls: {
                withCredentials: false,
              },
            },
            html5: {
              hls: {
                withCredentials: false,
              },
            },
            techOrder: ['html5', 'flash'],
            poster:
              'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1596173685045&di=76144e8eb9bc70c04b6f55dd1b4752ed&imgtype=0&src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fimages%2F20190109%2F352cd11ec90e4c6b9c20800562967137.jpeg',
            notSupportedMessage: '此视频暂无法播放,请稍后再试',
            controlBar: {
              timeDivider: true,
              durationDisplay: true,
              remainingTimeDisplay: false,
              fullscreenToggle: true,
            },
          };
        },
        set() {},
      },
    },
    data() {
      return {
        showTitleBar: false,
        Maximize: this.maximize,
      };
    },
    methods: {
      onPlayerPlay(e) {
        // 播放器开始播放事件,目前仅将player对象弹出给父组件
        this.showTitleBar = false;
        this.$emit('play', e);
      },

      onPlayerPause(e) {
        // 播放器暂停播放事件,同上
        this.showTitleBar = true;
        this.$emit('pause', e);
      },

      onPlayerEnded(e) {
        // 直播流没有结束的概念,留着备用
        this.$emit('end', e);
      },

      onVideoDBClick() {
        // 双击全屏
        if (!this.$refs.videoPlayer.player.isFullscreen()) {
          this.$refs.videoPlayer.player.requestFullscreen();
        }
      },

      onMaximize() {
        // 视频单个最大化,占满整个弹窗,类似于视频网站的网页全屏
        this.Maximize = !this.Maximize;
        this.$emit('maximize', this.Maximize);
      },

      onClose() {
        // 关闭视频事件
        this.$emit('close');
      },
    },
  };
</script>

<style lang="less">
  .rmtp-video-player {
    position: relative;

    .head-menu {
      position: absolute;
      width: 100%;
      color: #fff;
      top: 0;
      left: 0;
      right: 0;
      height: 25px;
      background-color: rgba(43, 51, 63, 0.7);
      z-index: 9;
      opacity: 0;

      &.showbar {
        //出现顶部条
        opacity: 1;
        transition: 0.5s;
      }

      .right-button {
        position: absolute;
        right: 5px;
        top: 2px;

        i {
          cursor: pointer;
          margin: 0 5px;
        }

        z-index: 3;
      }

      transition: 5s; //模仿底部条缓慢消失

      .app-title {
        display: inline-block;
        width: 100%;
      }
    }

    .video-player {
      cursor: pointer;

      .video-js {
        width: 100%;
      }

      .vjs-big-play-button {
        left: 0;
        right: 0;
        bottom: 0;
        top: 0;
        width: 1.5em;
        border-radius: 50%;
        margin: auto;
      }
    }
  }
</style>

    以上就是一个简单的视频播放组件的封装,是在vue-video-player的基础上进一步封装一些跟业务相关的处理逻辑,目前还在更新中,暂定为以上例子。由于对该插件还不是很熟悉,上述例子目前仅支持离线的rtmp播放。


# 3. 编写视频播放界面(网格模式与播放列表模式)

    这里主要是视频播放列表比较重要,弹窗部分就忽略了,可使用element-ui提供的el-dialogs或自己另外封装一个可以实现最大化最小化功能的弹窗都可,视频播放列表由于采用的是弹性布局的样式,必须从外到内都是弹性布局,省略外部弹窗的样式代码,具体如下:

<!--左边是带过滤搜索框的树,右边0为网格视频列表,1为普通缩略图播放列表-->
<template>
  <div class="demo-video-all">
    <div class="left" :class="{rightMaximize: maximize}">
      <app-tree
        ref="tree"
        :show-checkbox="active==='1'"
        default-expand-all
        :check-on-click-node="active==='1'"
        :data="videoBody"
        :default-checked-filter="() => true"
        :default-checked-keys="checkedKeys"
        node-key="id"
        :props="treeProps"
        :expand-on-click-node="false"
        @check-change="handleCheckChanged"
      />
    </div>
    <!--以下视频的最大化均是通过绝对定位完成,如遇到弹性布局的,设置not选择器把其他的都隐藏掉,最大化那个绝对定位铺满弹窗即可,不可丢弃弹性布局-->
    <div class="right">
      <div class="right-option">
        <i
          class="el-icon-s-grid"
          :class="{active: active == '0'}"
          @click="active = '0'"
          title="列表"
        ></i>
        <i
          class="el-icon-picture"
          :class="{active: active == '1'}"
          @click="active = '1'"
          title="缩略图"
        ></i>
      </div>
      <template v-if="active==='1'">
        <template v-if="checkedList[0]">
          <div
            class="first-of-all"
            :class="{maximize: checkedList[0].maximize, someOneMaximize: someOneMaximize}"
          >
            <rtmp-player
              :videoUrl="checkedList[0].uri"
              @play="onPlay($event,checkedList[0].id)"
              @maximize="onMaximize($event, checkedList[0].id)"
              @close="onClose(checkedList[0].id)"
              :ref=" `player-${checkedList[0].id}` "
            >
              <template slot="video-name">
                <app-title
                  :value="checkedList[0].name"
                  :italic="false"
                  :size="16"
                  textAlign="center"
                />
              </template>
            </rtmp-player>
          </div>
        </template>
        <template v-if="checkedList.length > 1">
          <div class="is-going-to-play" :class="{maximize: someOneMaximize}">
            <rtmp-player
              v-for="item in checkedList.slice(1)"
              :key="item.id"
              :videoUrl="item.uri"
              @play="onPlay($event, item.id)"
              @maximize="onMaximize($event, item.id)"
              @close="onClose(item.id)"
              :class="{'player-maximize': item.maximize}"
              :ref=" `player-${item.id}` "
            >
              <template slot="video-name">
                <app-title
                  :value="item.name"
                  :italic="false"
                  :size="14"
                  textAlign="center"
                />
              </template>
            </rtmp-player>
          </div>
        </template>
      </template>
      <!--以下网格化视频列表分页,是通过弹性布局加浮动合在一起完成-->
      <template v-if="active==='0'">
        <div class="demo-video-page">
          <div class="demo-video-list-page">
            <template v-for="item in videoListBody">
              <rtmp-player :videoUrl="item.uri" :key="item.id">
                <template slot="video-name">
                  <app-title
                    :value="item.name"
                    :italic="false"
                    :size="16"
                    textAlign="center"
                  />
                </template>
              </rtmp-player>
            </template>
          </div>
          <!--分页清除浮动,固定在弹窗底部-->
          <el-pagination
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
            :page-sizes="[12, 24, 60, 120]"
            :page-size="listPagination.pagesize"
            :current-page="listPagination.pageindex"
            layout="total, sizes, prev, pager, next, jumper"
            :total="total"
            class="pagination"
          >
          </el-pagination>
        </div>
      </template>
    </div>
  </div>
</template>

<script>
  import axios from '@/axios/request';
  import rtmpPlayer from '@/components/rtmp-player';
  export default {
    components: {
      rtmpPlayer,
    },
    props: {
      resizeBody: [Object],
      minimize: {
        type: Boolean,
        default: false,
      },
    },
    data() {
      return {
        loading: false,
        //树的 结点数据
        videoBody: [],
        //视频分页列表
        videoListBody: [],
        listPagination: {
          pageindex: 1,
          pagesize: 12,
        },
        //勾选的结点key
        checkedKeys: [],
        checkedList: [],
        pagination: {
          pageindex: 1,
          pagesize: 9999,
        },
        total: 0,
        treeProps: {
          label: 'name',
          children: 'children',
        },
        // 是否有某个视频最大化
        maximize: false,
        //缩略图还是网格
        active: '0',
      };
    },
    computed: {
      // 底部列表是否有 某个视频最大化
      someOneMaximize() {
        return this.checkedList.slice(1).some(item => item.maximize);
      },
    },
    created() {
      this.getVideoList();
      this.getVideoListPage();
    },
    methods: {
      getVideoList() {
        /**
         *    这里是左边的树结点数据的请求接口
         * */
        //   this.videoBody = response.data.item.list;
      },

      getVideoListPage() {
        /**
         *    这里是分页请求视频列表的接口请求
         * */
        //   this.total = response.data.total;
        //   this.videoListBody = response.data.list; // 视频列表
      },
      // 监听树节点的勾选
      handleCheckChanged(node, checked) {
        if (checked) {
          if (!this.checkedKeys.includes(node.id)) {
            this.checkedKeys.push(node.id);
            this.checkedList.push(
              Object.assign({}, node, {
                maximize: false,
              })
            );
          }
        } else {
          this.checkedKeys.splice(
            this.checkedKeys.findIndex(id => id === node.id),
            1
          );
          this.checkedList.splice(
            this.checkedList.findIndex(Node => Node.id === node.id),
            1
          );
        }
      },
      // 视频播放
      onPlay(player, id) {
        const playIndex = this.checkedList.findIndex(Node => Node.id === id);
        if (playIndex > 0) {
          const temp = this.checkedList[0];
          this.$set(this.checkedList, 0, this.checkedList[playIndex]);
          this.$set(this.checkedList, playIndex, temp);
        }
      },

      // 关闭视频,顺便取消树的勾选
      onClose(id) {
        const anotherIndex = this.checkedList.findIndex(Node => Node.id === id);
        const removeKeys = this.checkedList[anotherIndex];
        this.maximize = false;
        this.$nextTick(() => {
          this.$refs.tree.$children[1].setChecked(removeKeys, false);
        });
      },

      //最大化某个视频
      onMaximize(maximize, id) {
        const index = this.checkedList.findIndex(Node => Node.id === id);
        this.checkedList[index].maximize = maximize;
        this.maximize = maximize;
      },

      // 分页页数
      handleSizeChange(val) {
        this.listPagination.pagesize = val;
        this.getVideoListPage();
      },

      // 分页当前页
      handleCurrentChange(val) {
        this.listPagination.pageindex = val;
        this.getVideoListPage();
      },
    },
  };
</script>

<style lang="less">
  .demo-video-all {
    padding: 10px;
    flex: 1;
    display: flex;
    overflow: hidden;

    .el-tree {
      margin: 10px 0;
      background: transparent;
    }

    .el-tree-node__label {
      color: rgba(255, 255, 255, 0.8);
    }

    .el-tree-node__content:hover {
      background: rgba(0, 84, 160, 0.5) !important;
    }

    .el-tree-node.is-current .el-tree-node__content {
      background: transparent;
    }

    .app-tree__search .el-input__inner {
      background-color: transparent;
      width: 98%;
      border-radius: 0;
      line-height: 30px;
      border-color: rgba(255, 255, 255, 0.3);
      color: rgba(255, 255, 255, 0.8);

      &:focus {
        border-color: rgba(0, 84, 160, 0.8);
        box-shadow: 0 0 10px 3px rgba(0, 84, 160, 0.3);
      }
    }

    .left {
      width: 200px;
      border-right: 1px solid rgba(0, 84, 160, 0.8);
      padding: 0 15px 0 20px;
      opacity: 1;
      transition: 0.5s;

      &.rightMaximize {
        opacity: 0;
      }

      .el-tree {
        overflow-y: hidden;
        max-height: 600px;
        padding-right: 18px;

        &:hover {
          overflow-y: auto;
        }
      }
    }

    ::-webkit-scrollbar {
      width: 5px;
      height: 5px;
    }

    ::-webkit-scrollbar-thumb {
      border-radius: 8px;
      background: rgba(0, 84, 160, 0.8);
    }

    ::-webkit-scrollbar-track {
      border-radius: 8px;
    }

    .right {
      flex: 1;
      display: flex;
      flex-direction: column;
      overflow: hidden;

      .first-of-all {
        width: 65%;
        height: 100%;
        margin: 0 auto;
        display: flex;
        flex-direction: column;

        .rmtp-video-player {
          display: flex;
          flex-direction: column;
          flex: 1;

          .video-player {
            display: flex;
            flex-direction: column;
            flex: 1;

            .video-js {
              width: 100%;
              flex: 1;
              padding-top: 45%;
            }
          }
        }

        &.maximize {
          width: 100%;
          position: absolute;
          top: 0;
          left: 0;
          bottom: 0;
          right: 0;
          z-index: 9999;

          .rmtp-video-player {
            .video-player {
              .video-js {
                padding-top: 0;
              }
            }
          }
        }

        &.someOneMaximize {
          opacity: 0;
        }

        opacity: 1;
        transition: 0.5s;
      }

      .is-going-to-play {
        width: 100%;
        overflow: auto hidden;
        height: 250px;
        margin: 20px 10px;
        display: flex;
        flex-flow: row nowrap;

        &.maximize {
          flex: 1;
          width: 100%;
          height: 100%;
          position: absolute;
          left: -10px;
          top: -20px;
          z-index: 9999;
          overflow: hidden;
          transition: 0.5s;

          .rmtp-video-player {
            position: absolute;
            left: 0;
            margin: 0;
            padding: 0;
            right: 0;
            width: 100%;
            height: 100%;

            &:not(.player-maximize) {
              visibility: hidden;
              opacity: 0;
            }

            .video-player {
              display: flex;
              flex-direction: column;
              flex: 1;

              .video-js {
                padding-top: 0;
              }
            }
          }
        }

        .rmtp-video-player {
          flex: 0 0 250px;
          margin: 0 10px;
          display: flex;
          flex-direction: column;
          transition: 0.5s;

          .video-player {
            display: flex;
            flex-direction: column;
            flex: 1;

            .video-js {
              width: 100%;
              flex: 1;
            }
          }
        }
      }

      .right-option {
        position: absolute;
        right: 0;
        top: 0;
        z-index: 999;
        border-left: solid 1px #409eff;
        border-bottom: solid 1px #409eff;
        border-radius: 0 0 0 8px;
        width: 30px;

        i {
          display: inline-block;
          margin: 0 5px;
          font-size: 18px;
          line-height: 26px;
          cursor: pointer;

          &:hover {
            color: #1ccdcc;
          }

          &.active {
            color: #1ccdcc;
          }
        }
      }

      .demo-video-page {
        flex: 1;
        display: flex;
        flex-direction: column;
        overflow: hidden;

        .demo-video-list-page {
          flex: 1;
          overflow: hidden auto;

          .rmtp-video-player {
            float: left;
            position: relative;
            width: 300px;
            margin: 5px 12px;

            .head-menu {
              i {
                display: none;
              }
            }

            .video-player {
              display: flex;
              flex-flow: column wrap;

              .video-js {
                width: 100%;
                flex: 0 0 150px;
              }
            }
          }
        }

        .el-pagination {
          border-top: solid 1px rgba(0, 145, 202, 0.8);
          padding-bottom: 10px;
        }

        .pagination {
          text-align: center;
          border-top: none !important;
          margin: 5px auto;
          clear: both;
          display: block;
          width: 100%;
        }
      }
    }
  }
</style>

# 4. 编写视频播放界面(监控网格模式)

  以上这种模式,排版布局终归不是特别的好,而且还有交换导致的播放问题,后续更新为以下这种模式,网格从少到多的区分,兼容较好,保留右侧操纵视频的界面还未完成,暂定如下:

<template>
  <div class="demo-video-player-page">
    <div class="left-list">
      <app-tree
        ref="videoTree"
        v-loading="loading"
        show-checkbox
        default-expand-all
        check-on-click-node
        :data="videoData"
        :default-checked-filter="() => true"
        :default-checked-keys="checkedKeys"
        node-key="id"
        :props="treeProps"
        :expand-on-click-node="false"
        @check-change="onCheckedChange"
      />
    </div>
    <div class="middle-player-list">
      <div class="content" ref="videoContent">
        <div
          v-for="(item, index) in videoList"
          :key="index"
          :class="`video-player-${row}x${row}`"
        >
          <div v-if="item" @click.stop="activeNum=index" class="container">
            <rtmp-player
              :videoUrl="item.uri"
              v-if="item && item.uri"
              :class="{active: activeNum == index}"
            ></rtmp-player>
          </div>
          <div
            class="place-holder"
            v-else
            :class="{active: activeNum == index}"
            @click="activeNum = index"
          >
            <app-title
              :value="(index+1).toString()"
              :italic="false"
              :size="14"
              textAlign="center"
            />
          </div>
        </div>
      </div>
      <div class="footer">
        <i
          v-for="item in 4"
          :key="item"
          class="iconfont"
          :title="`${item}x${item}`"
          :class="[`icon-grid-${item}x${item}`, {active: row == item}]"
          :style="{fontSize: item < 3 ? '17px': ''}"
          @click="onShowGrid(item)"
        ></i>
        <i class="icon-svg-fullscreen" @click="onFullScreen" title="全屏"></i>
      </div>
    </div>
  </div>
</template>

<script>
  import axios from '@/axios/request';
  import rtmpPlayer from '@/components/rtmp-player';
  export default {
    name: 'VideoPlayer',
    components: {
      rtmpPlayer,
    },
    data() {
      return {
        videoList: [],
        row: 4,
        activeNum: -1,
        videoData: [],
        loading: false,
        checkedKeys: [],
        treeProps: {
          label: 'name',
          children: 'children',
        },
        pagination: {
          pageindex: 1,
          pagesize: 16,
        },
        isFullScreen: false,
      };
    },
    mounted() {
      this.onShowGrid(this.row);
      this.getVideoList();
    },
    methods: {
      onShowGrid(row) {
        this.row = row;
        this.activeNum = -1;
        const hasVideo = this.videoList.filter(Boolean); // 过滤出有视频数据的数组元素
        this.videoList = new Array(row ** 2); // 重新变形数组
        for (let i = 0; i < Math.min(hasVideo.length, row ** 2); i++) {
          this.videoList[i] = Object.assign([], hasVideo[i]);
        } // 按顺序赋值
        this.$nextTick(() => {
          this.$refs.videoTree.$children[1].setCheckedNodes(
            this.videoList,
            true
          );
        }); //重新渲染左边树的勾选情况
      },

      getVideoList() {
        /**
         * 这里是请求视频数据,并赋值给this.videoData
         * */
      },

      onCheckedChange(node, checked) {
        if (checked) {
          if (!this.checkedKeys.includes(node.id)) {
            if (this.activeNum >= 0) {
              //如果是已有的视频,执行替换逻辑
              const removeVideo = this.videoList[this.activeNum];
              if (removeVideo && removeVideo.id) {
                this.checkedKeys.splice(
                  this.checkedKeys.findIndex(item => item === removeVideo.id),
                  1
                );
                this.$nextTick(() => {
                  this.$refs.videoTree.$children[1].setChecked(
                    removeVideo,
                    false
                  );
                });
              }
            } else {
              this.activeNum = 0;
            }
            //否则替换黑框
            this.checkedKeys.push(node.id);
            this.videoList[this.activeNum] = Object.assign({}, node);
            //循环替换,防止出错
            this.activeNum = (this.activeNum + 1) % this.videoList.length;
          }
        } else {
          //如果左边取消了视频勾选,找到对应的数组元素删除
          this.checkedKeys.splice(
            this.checkedKeys.findIndex(id => id === node.id),
            1
          );
          //网格切换为黑框
          const index = this.videoList.findIndex(
            Node => Node && Node.id === node.id
          );
          if (index >= 0) {
            this.videoList[index] = undefined;
          }
        }
      },

      onFullScreen() {
        //中间整个监控块全屏
        this.isFullScreen = true;
        const videoDom = this.$refs.videoContent;
        if (this.isFullScreen) {
          //全屏
          if (videoDom.requestFullscreen) {
            videoDom.requestFullscreen();
          } else if (videoDom.webkitRequestFullScreen) {
            videoDom.webkitRequestFullScreen();
          } else if (videoDom.mozRequestFullScreen) {
            videoDom.mozRequestFullScreen();
          } else {
            videoDom.msRequestFullscreen();
          }
        } else {
          //退出全屏
          if (document.exitFullscreen) {
            document.exitFullscreen();
          } else if (document.mozCancelFullScreen) {
            document.mozCancelFullScreen();
          } else if (document.msExitFullscreen) {
            document.msExiFullscreen();
          } else if (document.webkitCancelFullScreen) {
            document.webkitCancelFullScreen();
          }
        }
      },
    },
  };
</script>

<!--全为弹性布局-->
<style lang="less">
  .demo-video-player-page {
    flex: 1;
    display: flex;
    color: #fff;

    .left-list {
      width: 13%;
      .el-tree {
        margin: 10px 0;
        background: transparent;
      }
      .el-tree-node__label {
        color: rgba(255, 255, 255, 0.8);
      }
      .el-tree-node__content:hover {
        background: rgba(0, 84, 160, 0.5) !important;
      }
      .el-tree-node.is-current .el-tree-node__content {
        background: transparent !important;
      }
      .app-tree__search .el-input__inner {
        background-color: transparent;
        width: 85%;
        border-radius: 0;
        line-height: 30px;
        border-color: rgba(255, 255, 255, 0.3);
        color: rgba(255, 255, 255, 0.8);
        &:focus {
          border-color: rgba(0, 84, 160, 0.8);
          box-shadow: 0 0 10px 3px rgba(0, 84, 160, 0.3);
        }
      }
    }

    .middle-player-list {
      flex: 1;
      display: flex;
      flex-direction: column;
      .content {
        max-height: calc(100% - 40px);
        flex: 1;
        display: flex;
        flex-flow: row wrap;
        .place-holder {
          flex: 1;
          background-color: rgba(0, 0, 0, 0.8);
          border: solid 1px #222;
          text-align: center;
          cursor: pointer;
          display: flex;
          flex-direction: column;
          justify-content: center;
          &.active {
            border: solid 1px #cdad00;
          }
        }
        .container {
          flex: 1;
          display: flex;
          flex-direction: column;
          .rmtp-video-player {
            border: solid 1px #222;
            &.active {
              border: solid 1px #cdad00;
              border-right: solid 1.5px #cdad00;
            }
          }
        }
        .video-player-3x3 {
          flex: 0 0 33.33%;
          display: flex;
          flex-direction: column;
        }

        .video-player-4x4 {
          flex: 0 0 25%;
          display: flex;
          flex-direction: column;
        }

        .video-player-2x2 {
          flex: 0 0 50%;
          display: flex;
          flex-direction: column;
        }

        .video-player-1x1 {
          display: flex;
          flex-direction: column;
          flex: 1;
        }
      }

      .footer {
        color: #fff;
        height: 40px;
        i {
          display: inline-block;
          line-height: 40px;
          padding: 0 5px;
          cursor: pointer;
          &:hover {
            color: #1ccdcc;
          }
          &:last-child {
            padding: 0;
            margin: 10px 5px;
            float: right;
          }
          &.active {
            color: #1ccdcc;
            box-shadow: 0 0 8px 3px rgba(0, 204, 255, 0.2) inset;
          }
        }
      }
    }
  }
</style>

   更改 rtmpPlayer 的配置后,目前已经能播放 m3u8 及 rtmp 格式的视频,因为涉及为直播,流不稳定及网络原因会导致比较明显的卡顿,这部分问题后续再看看如何处理, 大神们有更好的方法也欢迎留言区指教,目前的话暂定为网格模式来显示。