场景1:
想实现一个铺满视频的大屏,当获取到视频地址后,所有视频自动播放并且可以循环播放,在横屏大屏时展示5*3的数量,在竖屏大屏时展示3*3的数量,在每个视频下半部分展示一下文本或者图标(内容可自定义)
横屏效果和竖屏效果如下:
<template>
<div class="video-grid" :style="videoGridStyle">
<div v-for="(video, index) in videoList" :key="index" class="video-container">
<video :id="`player-${index}`" class="video-item" :poster="video.cover" playsinline muted loop autoplay></video>
<div class="bottom-shadow">
<div class="col-name-div">视频{{ index + 1 }}</div>
</div>
</div>
</div>
</template>
在下面的js部分代码中使用到的 sliceVideoList() 方法是因为我要实现这样的需求场景:
在横屏下只展示15个视频,在竖屏下只展示9个视频,至于我为什么请求200个数据这里是具体的需求不过多赘述。
<script setup>
import { onMounted, onBeforeUnmount, ref, nextTick, watch } from 'vue'
import Plyr from 'plyr'
import Hls from 'hls.js'
import 'plyr/dist/plyr.css'
import { getChartData } from "@/request/chart.js";
const videoList = ref([])
const fullList = ref([])
const players = []
const setupVideoPlayer = (videoEl, videoUrl) => {
const player = new Plyr(videoEl, {
iconUrl: '',
controls: ['play', 'fullscreen'],
settings: [],
fullscreen: {
enabled: true,
fallback: true,
iosNative: true
}
})
// 如果浏览器不支持在video标签中设置播放参数,可以在这里设置下面这三行内容
// videoEl.loop = true
// videoEl.autoplay = true
// videoEl.muted = true
const urlObj = new URL(videoUrl)
if (urlObj.pathname.endsWith('.m3u8')) {
if (Hls.isSupported()) {
const hls = new Hls()
hls.loadSource(videoUrl)
hls.attachMedia(videoEl)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('HLS 准备完成')
// player.play()
})
hls.on(Hls.Events.ERROR, (event, data) => {
console.error('HLS 错误:', data)
})
} else if (videoEl.canPlayType('application/vnd.apple.mpegurl')) {
videoEl.src = videoUrl
videoEl.addEventListener('loadeddata', () => {
// player.play()
})
} else {
console.error('不支持 HLS 播放')
}
} else {
videoEl.src = videoUrl
videoEl.addEventListener('loadeddata', () => {
// player.play()
})
}
return player
}
const initPlayers = async () => {
await nextTick()
players.length = 0
videoList.value.forEach((video, index) => {
const videoEl = document.getElementById(`player-${index}`)
if (!videoEl) return
const player = setupVideoPlayer(videoEl, video.url)
players.push(player)
})
}
const getPlayers = async () => {
const formData = {
limit: 200
}
getChartData(formData).then((res) => {
// 此处省略获取接口数据的代码...
fullList.value = res.data.list
refresh()
}).catch((err) => {
console.log(err)
})
}
const videoGridStyle = ref({})
const refresh = async () => {
sliceVideoList()
calcLayout()
await nextTick()
await initPlayers()
}
const sliceVideoList = () => {
const screenW = window.innerWidth
const screenH = window.innerHeight
const isLandscape = screenW >= screenH
const count = isLandscape ? 15 : 9
videoList.value = fullList.value.slice(0, count)
}
const calcLayout = () => {
const screenW = window.innerWidth
const screenH = window.innerHeight
const gap = 8
const padding = 16
const ratio = 9 / 16
const isLandscape = screenW >= screenH
const rows = 3
const cols = isLandscape ? 5 : 3
const maxW = screenW - padding - gap * (cols - 1)
const maxH = screenH - padding - gap * (rows - 1)
let videoW = maxW / cols
let videoH = videoW / ratio
if (videoH * rows > maxH) {
videoH = maxH / rows
videoW = videoH * ratio
}
videoGridStyle.value = {
gridTemplateColumns: `repeat(${cols}, ${videoW}px)`,
gridTemplateRows: `repeat(${rows}, ${videoH}px)`,
justifyContent: 'center',
alignContent: 'center',
width: '100vw',
height: '100vh',
overflow: 'hidden'
}
}
onMounted(() => {
getPlayers()
window.addEventListener('resize', refresh)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', refresh)
})
</script>
<style scoped>
.video-grid {
display: grid;
gap: 8px;
padding: 8px;
box-sizing: border-box;
background-color: #fff;
overflow: hidden;
}
.video-item {
width: 100%;
height: 100%;
object-fit: cover;
background-color: #fff;
}
.video-container {
position: relative;
overflow: hidden;
}
::v-deep(.plyr) {
width: 100% !important;
height: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
min-height: 0 !important;
}
::v-deep(.plyr__video-wrapper) {
width: 100% !important;
height: 100% !important;
}
.bottom-shadow {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: linear-gradient(
to top,
rgba(0, 0, 0, 1.0) 0%,
rgba(0, 0, 0, 0.6) 30%,
rgba(0, 0, 0, 0.3) 60%,
rgba(0, 0, 0, 0) 100%
);
color: white;
padding: 8px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.col-name-div {
width: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bottom-shadow-img {
display: flex;
flex-wrap: wrap;
margin-top: 6px;
gap: 4px;
}
.icon-wrapper {
position: relative;
}
.platform-logo {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.icon-corner {
position: absolute;
right: -6px;
bottom: -6px;
font-size: .8vw;
border-radius: 50%;
}
.icon-status {
position: absolute;
right: -6px;
bottom: -6px;
font-size: .8vw;
z-index: 2;
fill: #fff;
}
/* 竖屏 */
@media (orientation: portrait) {
.bottom-shadow {
font-size: 3vw;
min-height: 20%;
}
.icon-wrapper {
width: 4.4vw;
height: 4.4vw;
}
.icon-corner {
position: absolute;
right: -6px;
bottom: -6px;
font-size: 2.4vw;
border-radius: 50%;
}
.icon-status {
position: absolute;
right: -6px;
bottom: -6px;
font-size: 2.4vw;
z-index: 2;
fill: #fff;
}
}
/* 横屏 */
@media (orientation: landscape) {
.bottom-shadow {
font-size: 1.1vw;
min-height: 24%;
}
.icon-wrapper {
width: 1.5vw;
height: 1.5vw;
}
.icon-corner {
position: absolute;
right: -6px;
bottom: -6px;
font-size: 1vw;
border-radius: 50%;
}
.icon-status {
position: absolute;
right: -6px;
bottom: -6px;
font-size: 1vw;
z-index: 2;
fill: #fff;
}
}
.mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
z-index: 1;
}
</style>
场景2:
因为网络带宽等等的问题,自动播放所有视频的处理不够优雅,因此有了新的需求场景,每行默认播放第一个视频,播放结束后自动播放下一个,直到每行各自播放完最后一个视频后,再循环到第一个视频进行播放。
需要想到的问题是:
- 每行的轮播都是独立的;
- 这里利用 Plyr 的 .on('ended', fn) 监听播放完成事件;
- 用 Map<rowIndex, index> 记录每行当前播放的索引;
- 竖屏和横屏每行展示数量不同,需要进行计算区分。
html 部分只需改动 video 设置的几个属性,去掉 loop 和 autoplay:
<video :id="`player-${index}`" class="video-item" :poster="video.cover" playsinline muted></video>
<script setup>
import { onMounted, onBeforeUnmount, ref, nextTick, watch } from 'vue'
import Plyr from 'plyr'
import Hls from 'hls.js'
import 'plyr/dist/plyr.css'
import { getChartData } from "@/request/chart.js";
const videoList = ref([])
const fullList = ref([])
const players = []
const currentPlayingIndexByRow = new Map()
const setupVideoPlayer = (videoEl, videoUrl) => {
const player = new Plyr(videoEl, {
iconUrl: '',
controls: ['play', 'fullscreen'],
settings: [],
fullscreen: {
enabled: true,
fallback: true,
iosNative: true
}
})
videoEl.muted = true
videoEl.playsInline = true
videoEl.loop = false
videoEl.autoplay = false
if (videoUrl.endsWith('.m3u8') && Hls.isSupported()) {
const hls = new Hls()
hls.loadSource(videoUrl)
hls.attachMedia(videoEl)
} else {
videoEl.src = videoUrl
}
return player
}
// 播放指定 index 的视频,并暂停同一行其他的
const playVideoInRow = (rowIndex, videoIndexInRow) => {
const isLandscape = window.innerWidth >= window.innerHeight
const cols = isLandscape ? 5 : 3
const startIdx = rowIndex * cols
const endIdx = Math.min(startIdx + cols, players.length)
for (let i = startIdx; i < endIdx; i++) {
const player = players[i]
if (i === startIdx + videoIndexInRow) {
player.play()
currentPlayingIndexByRow.set(rowIndex, videoIndexInRow)
} else {
player.pause()
}
}
}
const handleEnded = (rowIndex) => {
const isLandscape = window.innerWidth >= window.innerHeight
const cols = isLandscape ? 5 : 3
const curIdx = currentPlayingIndexByRow.get(rowIndex) || 0
const next = (curIdx + 1) % cols
playVideoInRow(rowIndex, next)
}
const restartPlaybackForEachRow = () => {
const isLandscape = window.innerWidth >= window.innerHeight
const cols = isLandscape ? 5 : 3
const rowCount = Math.ceil(videoList.value.length / cols)
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
playVideoInRow(rowIndex, 0)
}
}
const initPlayers = async () => {
await nextTick()
players.length = 0
currentPlayingIndexByRow.clear()
const isLandscape = window.innerWidth >= window.innerHeight
const cols = isLandscape ? 5 : 3
for (let index = 0; index < videoList.value.length; index++) {
const videoEl = document.getElementById(`player-${index}`)
if (!videoEl) continue
const player = setupVideoPlayer(videoEl, videoList.value[index].url)
players.push(player)
const rowIndex = Math.floor(index / cols)
const colIndex = index % cols
player.on('ready', () => {
// 默认每行第一个播放
if (colIndex === 0) {
playVideoInRow(rowIndex, 0)
} else {
player.pause()
}
})
player.on('ended', () => {
handleEnded(rowIndex, cols)
})
}
}
const getPlayers = async () => {
const formData = { limit: 200 }
getChartData(formData).then((res) => {
// 省略接口数据处理部分代码
fullList.value = Array.from(pidMap.values())
refresh()
}).catch((err) => {
console.log(err)
})
}
const videoGridStyle = ref({})
const refresh = async () => {
sliceVideoList()
calcLayout()
await initPlayers()
restartPlaybackForEachRow()
}
const sliceVideoList = () => {
const screenW = window.innerWidth
const screenH = window.innerHeight
const isLandscape = screenW >= screenH
const count = isLandscape ? 15 : 9
videoList.value = fullList.value.slice(0, count)
console.log(videoList.value)
}
const calcLayout = () => {
const screenW = window.innerWidth
const screenH = window.innerHeight
const gap = 8
const padding = 16
const ratio = 9 / 16
const isLandscape = screenW >= screenH
const rows = 3
const cols = isLandscape ? 5 : 3
const maxW = screenW - padding - gap * (cols - 1)
const maxH = screenH - padding - gap * (rows - 1)
let videoW = maxW / cols
let videoH = videoW / ratio
if (videoH * rows > maxH) {
videoH = maxH / rows
videoW = videoH * ratio
}
videoGridStyle.value = {
gridTemplateColumns: `repeat(${cols}, ${videoW}px)`,
gridTemplateRows: `repeat(${rows}, ${videoH}px)`,
justifyContent: 'center',
alignContent: 'center',
width: '100vw',
height: '100vh',
overflow: 'hidden'
}
}
onMounted(() => {
getPlayers()
window.addEventListener('resize', refresh)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', refresh)
})
</script>
这里存在一个问题(可选),当我切换分辨率的时候,可能还有正在播放的视频,这里我要重新计算播放顺序的话,需要先暂停在播放中的视频,再去重新计算(去调用计算的方法,此处省略)。使用以下代码可以暂停:
const pauseAllPlayers = () => {
players.forEach(player => {
player.pause?.()
})
}
const refresh = async () => {
pauseAllPlayers()
sliceVideoList()
calcLayout()
await initPlayers()
}
补充:
如果需要点击列表中的某个视频置顶吸顶显示,在吸顶的 div 中定义的 list 在 v-for 的时候,不要使用 index 去作为 key 和 id,会因为 index 在数组中不变化导致视频数据不能正常更新:
<div v-for="(video, index) in topList" :key="video.pid">
<video :id="`sticky-player-${video.pid}`" preload="none" :poster="video.cover" playsinline muted autoplay loop></video>
</div>