概述
最近由于工作需要,要做一个支持多分屏的视频播放页面,并且支持视频全屏和视频云台控制。本篇文章重点记录下窗口的多分屏、云台控制、视频播放这些前端实现,视频播放地址获取接口、云台控制接口还需根据自己的业务去实现,不是本文关注的重点。
xgplayer 西瓜播放器是字节跳动开源的一个Web视频播放器类库,它本着一切都是组件化的原则设计了独立可拆卸的 UI 组件。更重要的是它不只是在 UI 层有灵活的表现,在功能上也做了大胆的尝试:摆脱视频加载、缓冲、格式支持对 video 的依赖。尤其是在 mp4 点播上做了较大的努力,让本不支持流式播放的 mp4 能做到分段加载,这就意味着可以做到清晰度无缝切换、加载控制、节省视频流量。同时,它也集成了对 flv、hls、dash 的点播和直播支持。
Layui 是一套免费的开源 Web UI 组件库,采用自身轻量级模块化规范,遵循原生态的 HTML/CSS/JavaScript 开发模式,极易上手,拿来即用。其风格简约轻盈,而内在雅致丰盈,甚至包括文档在内的每一处细节都经过精心雕琢,非常适合网页界面的快速构建。Layui 区别于一众主流的前端框架,却并非逆道而行,而是信奉返璞归真之道。确切地说,它更多是面向于追求简单的务实主义者,即无需涉足各类构建工具,只需面向浏览器本身,便可将页面所需呈现的元素与交互信手拈来。
Layui 进行页面布局,xgplayer 播放视频,两者配合,那么实现视频窗口多分屏就不是难事了啦!!!
多分屏
多分屏使用 Layui 栅格布局,栅格布局代码可以直接去官网复制。如果只有一个视频,则用 layui-col-xs12 样式,多于一个视频则用 layui-col-xs6 样式,布局始终等比例水平排列。
<div class="layui-row">
<div th:class="${channelIdsArray.length==1}?'layui-col-xs12':'layui-col-xs6'"
th:each="channelId,channelIdStat : ${channelIdsArray}">
......
</div>
</div>
云台控制
光标移动到哪个视频上,则哪个视频的右下角显示云台控制按钮,光标移出视频则隐藏。同样使用 Layui 栅格布局做一个九宫格,然后再自己通过 CSS 样式把九宫格变成一个圆盘。
.controlPtz {
position: absolute;
right: 20px;
bottom: 50px;
z-index: 999;
background-color: #D5D8E0;
width: 150px;
height: 150px;
border-radius: 50%;
border: 2px solid #BDBDBD;
clip-path: circle(77px at center);
display: none;
}
.controlPtzGrid {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.controlPtzGridCenter {
border-radius: 50%;
border: 1px solid #BDBDBD;
width: 48px;
height: 48px;
}
.controlImg {
max-width: 20px;
}
.controlImg:hover {
transform: scale(1.8); /* 鼠标悬停时放大10% */
z-index: 2; /* 放大后提高层级 */
}
视频播放
xgplayer 进行视频播放,具体的配置项可以去官网查看。
let player = new Player({
id: 'mse' + (i + 1),
isLive: true,
playsinline: true,
url: url,
autoplayMuted: true,
autoplay: true,
pip: false, // 是否使用画中画插件
miniprogress: false,
screenShot: false,
playbackRate: false,
cssFullscreen: false,
controls: {
mode: 'flex'
},
// fitVideoSize: 'auto',
videoFillMode: 'fill',
height: innerHeight,
width: innerWidth,
plugins: [scheme === 'FLV_HTTP' ? window.FlvPlayer : window.HlsPlayer],
hls: {
disconnectTime: 60 // 直播断流时间,默认 0 秒,(独立使用时等于 maxLatency)
},
fullscreenTarget: channelIdsArray.length === 1 ? document.documentElement : null
});
此处监听播放器错误事件,如果播放地址过期了,就重新获取新的播放地址。
player.on(Events.ERROR, (error) => {
console.log(error);
let httpCode = error.httpCode;
// 播放地址过期了,重新获取新的地址
if (httpCode === 401) {
$.ajax({
url: ctxPath + 'dssVideo/getPlayVideoUrl',
type: 'POST',
dataType: 'json',
contentType: 'application/json;charset=UTF-8',
data: JSON.stringify({
'channelIds': [channelId],
'scheme': scheme // FLV_HTTP、HLS
}),
success: function (result) {
let data = result.data;
let url = data[0].url;
player.config.url = player.src = url;
player.play();
}
});
}
});
完整前端代码
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>播放器</title>
<meta name=viewport
content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,minimal-ui">
<meta name="referrer" content="no-referrer">
<link th:href="@{/lib/layui/css/layui.css}" rel="stylesheet"/>
<link th:href="@{/lib/xgplayer/index.min.css}" rel="stylesheet"/>
<style>
html, body {
width: 100%;
height: 100%;
margin: auto;
overflow-y: auto;
}
body {
display: flex;
}
.controlPtz {
position: absolute;
right: 20px;
bottom: 50px;
z-index: 999;
background-color: #D5D8E0;
width: 150px;
height: 150px;
border-radius: 50%;
border: 2px solid #BDBDBD;
clip-path: circle(77px at center);
display: none;
}
.controlPtzGrid {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.controlPtzGridCenter {
border-radius: 50%;
border: 1px solid #BDBDBD;
width: 48px;
height: 48px;
}
.controlImg {
max-width: 20px;
}
.controlImg:hover {
transform: scale(1.8); /* 鼠标悬停时放大10% */
z-index: 2; /* 放大后提高层级 */
}
</style>
<script type="text/javascript" th:inline="javascript">
document.addEventListener('DOMContentLoaded', () => {
resize();
const resizeObserver = new ResizeObserver(() => {
resize();
});
resizeObserver.observe(document.body);
});
function resize() {
const channelIdsArray = [[${channelIdsArray}]];
for (let i = 0; i < channelIdsArray.length; i++) {
let mse = document.getElementById('mse' + (i + 1));
let clientWidth = channelIdsArray.length === 1 ? document.body.clientWidth : document.body.clientWidth / 2;
let clientHeight = channelIdsArray.length > 4 ? (9 * clientWidth) / 16 : document.body.clientHeight / 2;
clientHeight = channelIdsArray.length === 1 ? document.body.clientHeight : clientHeight;
mse.style.width = clientWidth + 'px';
mse.style.height = clientHeight + 'px';
}
}
</script>
</head>
<body>
<div class="layui-row">
<div th:class="${channelIdsArray.length==1}?'layui-col-xs12':'layui-col-xs6'"
th:each="channelId,channelIdStat : ${channelIdsArray}">
<div th:id="'media'+ ${channelIdStat.count}"
th:onmouseenter="'controlPtzShow('+${channelIdStat.count}+');'" onmouseenter="controlPtzShow(0);"
th:onmouseleave="'controlPtzHide('+${channelIdStat.count}+');'" onmouseleave="controlPtzHide(0);">
<div th:id="'mse' + ${channelIdStat.count}"></div>
<div th:id="'controlPtz'+ ${channelIdStat.count}" class="controlPtz">
<div class="layui-col-xs4">
<div id="leftTop" onclick="controlPtz(this,'8');" class="controlPtzGrid" th:data="${channelId}"
style="justify-content: flex-end;align-items: flex-end;">
<img class="controlImg" th:src="@{/modules/dss/images/left_top.png}" alt="">
</div>
</div>
<div class="layui-col-xs4">
<div id="top" onclick="controlPtz(this,'1');" class="controlPtzGrid" th:data="${channelId}"
style="align-items: center;">
<img class="controlImg" th:src="@{/modules/dss/images/top.png}" alt="">
</div>
</div>
<div class="layui-col-xs4">
<div id="rightTop" onclick="controlPtz(this,'2');" class="controlPtzGrid" th:data="${channelId}"
style="justify-content: flex-start;align-items: flex-end;">
<img class="controlImg" th:src="@{/modules/dss/images/right_top.png}" alt="">
</div>
</div>
<div class="layui-col-xs4">
<div id="left" onclick="controlPtz(this,'7');" class="controlPtzGrid" th:data="${channelId}"
style="justify-content: center;">
<img class="controlImg" th:src="@{/modules/dss/images/left.png}" alt="">
</div>
</div>
<div class="layui-col-xs4">
<div class="controlPtzGrid controlPtzGridCenter">
<div class="layui-col-xs6">
<div id="amplify" onclick="controlPtz(this,'9');" th:data="${channelId}">
<img class="controlImg" th:src="@{/modules/dss/images/amplify.png}" alt=""></div>
</div>
<div class="layui-col-xs6">
<div id="reduce" onclick="controlPtz(this,'10');" th:data="${channelId}">
<img class="controlImg" th:src="@{/modules/dss/images/reduce.png}" alt=""></div>
</div>
</div>
</div>
<div class="layui-col-xs4">
<div id="right" onclick="controlPtz(this,'3');" class="controlPtzGrid" th:data="${channelId}"
style="justify-content: center;">
<img class="controlImg" th:src="@{/modules/dss/images/right.png}" alt="">
</div>
</div>
<div class="layui-col-xs4">
<div id="leftBottom" onclick="controlPtz(this,'6');" class="controlPtzGrid" th:data="${channelId}"
style="justify-content: flex-end;align-items: flex-start;">
<img class="controlImg" th:src="@{/modules/dss/images/left_bottom.png}" alt="">
</div>
</div>
<div class="layui-col-xs4">
<div id="bottom" onclick="controlPtz(this,'5');" class="controlPtzGrid" th:data="${channelId}"
style="align-items: center;">
<img class="controlImg" th:src="@{/modules/dss/images/bottom.png}" alt="">
</div>
</div>
<div class="layui-col-xs4">
<div id="rightBottom" onclick="controlPtz(this,'4');" class="controlPtzGrid" th:data="${channelId}"
style="justify-content: flex-start;align-items: flex-start;">
<img class="controlImg" th:src="@{/modules/dss/images/right_bottom.png}" alt="">
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" th:src="@{/lib/layui/layui.js}"></script>
<script th:src="@{/lib/xgplayer/index.min.js}"></script>
<script th:src="@{/lib/xgplayer/xgplayer-hls.js}"></script>
<script th:src="@{/lib/xgplayer/xgplayer-flv.js}"></script>
<script type="text/javascript" th:src="@{/lib/jquery/jquery-3.1.1.min.js}"></script>
<script type="text/javascript" th:inline="javascript">
// 项目根路径
const ctxPath = /*[[@{/}]]*/'';
const channelIdsArray = [[${channelIdsArray}]];
let innerWidth = channelIdsArray.length === 1 ? window.innerWidth : window.innerWidth / 2;
let innerHeight = channelIdsArray.length > 4 ? (9 * innerWidth) / 16 : window.innerHeight / 2;
innerHeight = channelIdsArray.length === 1 ? window.innerHeight : innerHeight;
const scheme = [[${scheme}]];
const Events = window.Player.Events;
// 用一个map保存所有的视频播放器对象
let players = new Map();
$.ajax({
url: ctxPath + 'dssVideo/getPlayVideoUrl',
type: 'POST',
dataType: 'json',
contentType: 'application/json;charset=UTF-8',
data: JSON.stringify({
'channelIds': channelIdsArray,
'scheme': scheme // FLV_HTTP、HLS
}),
success: function (result) {
let data = result.data;
for (let i = 0; i < data.length; i++) {
let url = data[i].url;
let channelId = data[i].channelId;
let player = new Player({
id: 'mse' + (i + 1),
isLive: true,
playsinline: true,
url: url,
autoplayMuted: true,
autoplay: true,
pip: false, // 是否使用画中画插件
miniprogress: false,
screenShot: false,
playbackRate: false,
cssFullscreen: false,
controls: {
mode: 'flex'
},
// fitVideoSize: 'auto',
videoFillMode: 'fill',
height: innerHeight,
width: innerWidth,
plugins: [scheme === 'FLV_HTTP' ? window.FlvPlayer : window.HlsPlayer],
hls: {
disconnectTime: 60 // 直播断流时间,默认 0 秒,(独立使用时等于 maxLatency)
},
fullscreenTarget: channelIdsArray.length === 1 ? document.documentElement : null
});
players.set(channelId, player);
}
for (const [channelId, player] of players) {
// 监听播放器错误事件
player.on(Events.ERROR, (error) => {
console.log(error);
let httpCode = error.httpCode;
// 播放地址过期了,重新获取新的地址
if (httpCode === 401) {
$.ajax({
url: ctxPath + 'dssVideo/getPlayVideoUrl',
type: 'POST',
dataType: 'json',
contentType: 'application/json;charset=UTF-8',
data: JSON.stringify({
'channelIds': [channelId],
'scheme': scheme // FLV_HTTP、HLS
}),
success: function (result) {
let data = result.data;
let url = data[0].url;
player.config.url = player.src = url;
player.play();
}
});
}
});
}
}
});
// 显示云台
function controlPtzShow(index) {
$("#controlPtz" + index).show();
}
// 隐藏云台
function controlPtzHide(index) {
$("#controlPtz" + index).hide();
}
// 云台控制
function controlPtz(that, control) {
let channelId = that.getAttribute('data');
$.ajax({
url: ctxPath + 'dssVideo/controlPtz',
type: 'POST',
dataType: 'json',
contentType: 'application/json;charset=UTF-8',
data: JSON.stringify({
'channelId': channelId,
'control': control
}),
success: function (result) {
}
});
}
</script>
</body>
</html>
评论