目录

    xgplayer + Layui 实现视频窗口多分屏


    xgplayer + Layui 实现视频窗口多分屏

    概述

    最近由于工作需要,要做一个支持多分屏的视频播放页面,并且支持视频全屏和视频云台控制。本篇文章重点记录下窗口的多分屏、云台控制、视频播放这些前端实现,视频播放地址获取接口、云台控制接口还需根据自己的业务去实现,不是本文关注的重点。

    xgplayer 西瓜播放器是字节跳动开源的一个Web视频播放器类库,它本着一切都是组件化的原则设计了独立可拆卸的 UI 组件。更重要的是它不只是在 UI 层有灵活的表现,在功能上也做了大胆的尝试:摆脱视频加载、缓冲、格式支持对 video 的依赖。尤其是在 mp4 点播上做了较大的努力,让本不支持流式播放的 mp4 能做到分段加载,这就意味着可以做到清晰度无缝切换、加载控制、节省视频流量。同时,它也集成了对 flv、hls、dash 的点播和直播支持。

    Layui 是一套免费的开源 Web UI 组件库,采用自身轻量级模块化规范,遵循原生态的 HTML/CSS/JavaScript 开发模式,极易上手,拿来即用。其风格简约轻盈,而内在雅致丰盈,甚至包括文档在内的每一处细节都经过精心雕琢,非常适合网页界面的快速构建。Layui 区别于一众主流的前端框架,却并非逆道而行,而是信奉返璞归真之道。确切地说,它更多是面向于追求简单的务实主义者,即无需涉足各类构建工具,只需面向浏览器本身,便可将页面所需呈现的元素与交互信手拈来。

    Layui 进行页面布局,xgplayer 播放视频,两者配合,那么实现视频窗口多分屏就不是难事了啦!!!

    xgplayer + Layui 实现视频窗口多分屏

    多分屏

    多分屏使用 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>
    
    end
  1. 作者: 锋哥 (联系作者)
  2. 发表时间: 2024-01-17 16:39
  3. 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  4. 转载声明:如果是转载博主转载的文章,请附上原文链接
  5. 公众号转载:请在文末添加作者公众号二维码(公众号二维码见右边,欢迎关注)
  6. 评论

    站长头像 知录

    你一句春不晚,我就到了真江南!

    文章0
    浏览0

    文章分类

    标签云