# 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>