# ZeenPlayer - компонент плеера

Компонент для вставки плеера на страницы сайта

# Пример:

# props

Название Описание Обязательны По умолчанию Что может принимать
src строка с путем воспроизведения + - любой путь к файлу - автоматическое определение типа файла - по умолчанию потоковое видео
type строка с типом файла - application/x-mpegURL mime-type файла
sources масив ресурсов для воспроизведения - - если нужно дать браузеру несколько потоков - используется этот параметр - масив должен содержать обьекты описания {src: 'путь файла', type:'mime-type файла', res:'Что за значение ресурса', label:"лейбл ресурса"}
poster изображение для плеера - false путь к любому изображению
autoplay включать автовоспроизведение плеера (работает только с muted) - - true/false
muted выключить звук у плеера - false true/false
range ограничитель для live video не позволит прокресбару выйти дальше чем есть - - масив из 1 или 2х значений - если 1 то это старт полосы если 2 то старт и конец
languages масив валидных обьектов языка - - пример можно найти тут: https://www.npmjs.com/package/videojs-language-switch
mainColor цвет плеера - берет значение из темы ($player-text-color) любой корректный цвет - #f00
btnColor цвет большой кнопки плеера - берет значение из темы ($player-btn-color) любой корректный цвет - #f00
progressColor цвет прогресс бара - берет значение из темы ($player-progress-color) любой корректный цвет - #f00
backgroundColor цвет подложки плеера - берет значение из темы ($player-background-color) любой корректный цвет - #f00

# slots - слоты используемые в компоненте

# play-btn - слот для размещения кнопки плей принимает параметры

  • play - функцию для старта воспроизведения плеера

# modules - слот для добавления любых модулей которые будут находиться рядом в плеером

# Source Code - исходный код компонента

<template>
  <div :id="`z-player-${componentId}`" class="zeen-player" ref="videoCover">
    <video class="video-js vjs-zeen video-js-player" style="width: 100%" ref="video" :poster="poster" playsInline />
    <slot name="play-btn" :play="play" v-if="!isPlaying">
      <div class="zeen-player__btn" @click.prevent="play()">
        <span class="icon-play"></span>
      </div>
    </slot>
    <slot name="modules" />
  </div>
</template>

<script>
const defaultSourceType = 'application/x-mpegURL'

let isFirstPlay = true
export default {
  name: 'ZeenPlayer',
  props: {
    src: {
      type: String,
      required: true,
    },
    sources: Array,
    type: {
      type: String,
      default: defaultSourceType,
    },
    poster: String,
    autoplay: {
      type: Boolean,
      default: false,
    },
    muted: {
      type: Boolean,
      default: false,
    },
    range: Array,
    languages: Array,
    onLoad: {
      type: Function,
      default: () => {},
    },
    time: Number,
    staticVideo: Boolean,
    volume: {
      type: Number,
      default: 1,
    },
  },
  data() {
    return {
      isPlaying: false,
      hasError: false,
      savedTime: -100,
    }
  },
  mounted() {
    import('video.js').then((module) => {
      const videojs = module.default
      // use code
      this.initPlayer(videojs)
      this.initPlayerEvents()
      this.initPlayerSource()
      this.initTime()

      this.onLoad()
    })
  },
  destroyed() {
    if (this.player) {
      this.offPlayerEvents()
      this.player.dispose()
    }
  },
  methods: {
    play() {
      if (this.hasError) {
        return
      }
      this.player.play()
    },
    toLive() {
      const isPlaying = this.isPlaying
      const player = this.player

      if (this.staticVideo) {
        return
      }

      if (isPlaying) {
        this.play()
      }

      // // player.src({type: player.currentType(), src: player.currentSrc()})

      setTimeout(() => {
        if (player.liveTracker.isLive()) {
          try {
            this.$refs.videoCover.querySelector('.vjs-seek-to-live-control').click()
          } catch (e) {
            console.log(e)
          }
        }
      }, 1000)
    },
    initPlayer(videojs) {
      this.player = videojs(this.$refs.video, {
        liveui: true,
        controls: true,

        autoplay: this.autoplay,
        muted: this.muted,
        preload: 'auto',
        poster: this.poster,
        playsinline: true,

        textTrackSettings: false,

        controlBar: {
          volumePanel: {inline: false},
        },

        html5: {
          nativeTextTracks: false,
          allowSeeksWithinUnsafeLiveWindow: true,
          vhs: {
            allowSeeksWithinUnsafeLiveWindow: true,
            withCredentials: false,
          },
        },
        liveTracker: {
          trackingThreshold: 40,
          liveTolerance: 40,
        },
      })

      // что то для обрезания продолжительности дорожки - плохо работало с лайвом
      // this.player.seekable = () => {
      //   const seekableBase = this.$refs.video.seekable

      //   if (this.range && this.range.length) {
      //     const start = this.range[0]
      //     const end = this.range[1] ? this.range[1] : seekableBase.end(0)

      //     return videojs.createTimeRanges(start, end)
      //   }

      //   return seekableBase
      // }
    },
    initPlayerEvents() {
      this.player.on('ready', this._playerReady)
      this.player.on('play', this._playerPlay)
      this.player.on('pause', this._playerPause)
      this.player.on('stop', this._playerStop)
      this.player.on('timeupdate', this._timeUpdate)
      this.player.on('waiting', this._playerWaiting)
      this.player.on('error', this._playerError)
      this.player.on('segmentnotfound', this._playerError)
      this.player.on('contentprotectionerror', this._playerError)
      this.player.on('aderror', this._playerError)
      this.player.on('offline', this._playerError)
      this.player.on('NoSupportedRepresentationFoundEvent', this._playerError)
      this.player.on('volumechange', this._playerVolume)
    },
    offPlayerEvents() {
      this.player.off('ready', this._playerReady)
      this.player.off('play', this._playerPlay)
      this.player.off('pause', this._playerPause)
      this.player.off('stop', this._playerStop)
      this.player.off('timeupdate', this._timeUpdate)
      this.player.off('waiting', this._playerWaiting)
      this.player.off('error', this._playerError)
      this.player.off('segmentnotfound', this._playerError)
      this.player.off('contentprotectionerror', this._playerError)
      this.player.off('aderror', this._playerError)
      this.player.off('offline', this._playerError)
      this.player.off('NoSupportedRepresentationFoundEvent', this._playerError)
      this.player.off('volumechange', this._playerVolume)
    },
    _playerError(eventData) {
      this.$emit('error', eventData)
    },
    _playerWaiting() {
      this.$emit('waiting', this.player)
    },
    _playerReady() {
      this.player.vhs = null
      this.$emit('ready', this.player)
    },
    _playerPlay() {
      this.isPlaying = true
      this.$emit('play', this.player)
      if (isFirstPlay && !this.time) {
        this.toLive()
      } else if (this.time > 1) {
        this.player.currentTime(this.time)
      }
      isFirstPlay = false
    },
    _playerPause() {
      this.isPlaying = false
      this.$emit('pause', this.player)
    },
    _playerStop() {
      this.isPlaying = false
      this.$emit('stop')
    },
    _timeUpdate() {
      const player = this.player
      let currentTime = player.currentTime()
      let duration = player.duration()

      if (player.liveTracker.isLive()) {
        duration = player.liveTracker.liveCurrentTime()
      }

      this.$emit('time-update', {
        currentTime,
        duration,
      })
    },
    _playerVolume() {
      this.$emit('volume', this.player.volume())
      this.$emit('muted', this.player.muted())
    },
    initPlayerSource() {
      const src = this.src
      const currentTime = this.player.currentTime()
      const isPlaying = this.isPlaying
      let isLive = false

      try {
        isLive = !!this.player.liveTracker.atLiveEdge()
      } catch (e) {
        console.error(e)
      }

      this.player.src({
        src,
        type: this.type ? this.type : this.getTypeForSource(src),
      })

      if (currentTime > 1 && !isLive) {
        if (document.querySelector('.vjs-live')) {
          this.player.currentTime(currentTime)
        } else {
          setTimeout(() => {
            this.player.currentTime(currentTime)
          }, 100)
        }
      }

      if (isPlaying) {
        this.play()
      }

      let sources = this.sources
      if (sources && sources.length) {
        sources = sources
          .map((source) => {
            return {
              src: source.src,
              type: source.type ? source.type : this.getTypeForSource(source.src),
              res: source.res,
              label: source.label,
            }
          })
          .filter((source) => source.src)

        this.player.src(sources)
      }
    },
    getTypeForSource(src) {
      const srcArs = src.split('.')
      if (!srcArs.length) {
        return defaultSourceType
      }
      const type = srcArs[srcArs.length - 1]

      switch (type.toLowerCase()) {
        case '3gpp2':
        case '3g2':
          return 'video/3gpp2'
        case '3gpp':
        case '3gp':
          return 'video/3gpp'
        case 'avi':
          return 'video/x-msvideo'
        case 'flv':
          return 'video/x-flv'
        case 'wmv':
          return 'video/x-ms-wmv'
        case 'webm':
          return 'video/webm'
        case 'mov':
          return 'video/quicktime'
        case 'mp4':
          return 'video/mp4'
        case 'ogv':
        case 'ogg':
          return 'video/ogg'
        case 'mpg':
        case 'mpeg':
        case 'mp1':
        case 'mp2':
        case 'mp3':
        case 'm1v':
        case 'mpv':
        case 'm1a':
        case 'm2a':
        case 'mpa':
          return 'video/mpeg'
        case 'm3u8':
        default:
          return defaultSourceType
      }
    },
    initTime() {
      const time = this.time
      const player = this.player
      const timeDiff = Math.max(time, this.savedTime) - Math.min(time, this.savedTime)
      this.savedTime = time
      // если измение между точками меньше 2 то ничего не делаем
      if (timeDiff < 2) {
        return
      }
      // не включаем перемотку если нет плеера или время 0
      if (!player || !time) {
        return
      }

      this.player.one('play', () => {
        this.player.currentTime(time)
        setTimeout(() => {
          this.player.currentTime(time)
        }, 100)
      })

      this.player.pause()
      this.play()
    },
  },
  computed: {
    componentId() {
      return this._uid
    },
  },
  watch: {
    //отслеживание обновления постера
    poster(newVal) {
      this.player.poster(newVal)
    },
    // отслеживание обновления основного пути воспроизведения
    src() {
      this.initPlayerSource()
    },
    time() {
      this.initTime()
    },
  },
}
</script>

<style lang="scss">
:root {
  /* Размеры */
  --player-aspect-ration: 56.52%;
  --player-btn-size: 40px;
  --player-btn-radius: 50%;
  --player-btn-font-size: 24px;

  /* Цвета */
  --player-main-color: var(--main-color);
  --player-background-color: var(--main-positive-color);
  --player-play-btn-color: var(--player-main-color);
  --player-progress-color: var(--player-main-color);
  --player-btn-background-color: var(--gray-4);
}
</style>

<style lang="scss">
@import '~video.js/dist/video-js.min.css';
@import '~@silvermine/videojs-quality-selector/dist/css/quality-selector';
</style>

<style lang="scss" scoped>
@import '../../fonts/zeen-ui-v1.0/style.css';

.zeen-player {
  position: relative;
  width: 100%;

  &::after {
    content: '';
    width: 100%;
    position: relative;
    padding-top: var(--player-aspect-ration);
    display: block;
    z-index: -1;
  }

  .vjs-zeen {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
  }

  ::v-deep video {
    outline: none;
    object-fit: cover;
  }

  &__btn {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--player-btn-size);
    height: var(--player-btn-size);
    border-radius: var(--player-btn-radius);
    font-size: var(--player-btn-font-size);
  }
}

.vjs-zeen ::v-deep {
  & .vjs-remaining-time {
    display: inline-flex;
    justify-content: center;
    align-items: center;
  }

  & .vjs-control {
    &::before {
      display: none;
    }
  }

  & .vjs-live-control {
    align-items: center;
  }

  .vjs-poster {
    background-size: cover;
  }

  &.video-js {
    color: var(--player-main-color);
  }

  div[class*='language-'] {
    background-color: transparent;
  }

  .vjs-big-play-button {
    border-color: var(--player-play-btn-color);
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: var(--player-btn-size);
    background-color: var(--player-btn-background-color);
    z-index: 1;
  }

  .vjs-volume-level,
  .vjs-play-progress,
  .vjs-slider-bar {
    background: var(--player-progress-color);
  }

  .vjs-control-bar {
    background: var(--player-background-color);
    height: 46px;
  }

  .vjs-button > .vjs-icon-placeholder:before {
    line-height: 46px;
  }

  .vjs-button {
    .vjs-time-control {
      line-height: 46px;
    }
  }
}
</style>

# Зависимости кода