后台管理增加两套通用播放器

This commit is contained in:
晚风拂柳颜 2023-06-02 14:35:45 +08:00
parent 88d71b6bc1
commit b5b76fad8b
25 changed files with 728 additions and 13 deletions

3
.idea/misc.xml generated
View File

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="FLOW" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (dr_py) (2)" project-jdk-type="Python SDK" />
</project>

Binary file not shown.

View File

@ -46,6 +46,15 @@ def custom_static_player(filename):
# print(filename)
return send_from_directory('templates/player', filename)
@web.route('/player1')
def custom_player1():
ctx = getParmas()
return render_template('player/mui/index.html', ctx=ctx)
@web.route('/player2')
def custom_player2():
ctx = getParmas()
return render_template('player/p2p-media-loader/p2pm3u8.html', ctx=ctx)
@web.route('/<web_name>/<theme>')
def web_index(web_name, theme):

View File

@ -1,3 +1,6 @@
###### 2023/06/02
- [X] 3.9.42beta22 后台管理增加两套通用在线播放器地址
###### 2023/05/12
- [X] 3.9.42beta1 后端代理解决302跨域问题,支持部分源的网页播放功能,由于无法解决嗅探跨域问题,网页版项目终结

View File

@ -1 +1 @@
{"showTime":89200000,"txt":"drpy 3.9.42beta1 -道长"}
{"showTime":89200000,"txt":"drpy 3.9.42beta22 -道长"}

View File

@ -1 +1 @@
3.9.42beta21
3.9.42beta22

View File

@ -124,7 +124,17 @@ body {
font-size: 15px;
padding-left: 5px
}
.btn-player{
border-radius: 25px;
font-size: 15px;
padding-left: 5px;
padding-right: 5px;
text-align: center;
border: #1e98d4;
color: #FFFFFF;
text-decoration: none;
background-image: linear-gradient(to right, rgb(71, 74, 252), rgb(252, 70, 243));
}
.ver {
font-size: 16px;
margin-left: 5px;

View File

@ -214,6 +214,7 @@
<div class="title">欢迎使用DR-PY管理界面<div><span class="ver_title">当前版本: {{ ver }}</span><span
class="ver_title">框架开发:道长</span><span class="ver_title">框架美化:蓝莓</span></div>
</div>
<a href="/web/player1?url=" class="btn-player" target="_blank">MUI播放器</a> <a href="/web/player2?url=" class="btn-player" target="_blank">P2P播放器</a>
<div class="nav">
<!-- 列表 -->
<ul>

View File

@ -13,16 +13,17 @@ function player(config){
MPlayer(config.url,config.title,config.vkey,config.next);
}else{
$.ajaxSettings.timeout='30000';
$.post("api_config.php", {"url":config.url,"time":config.time,"key":config.key,"title":config.title},
function(data) {
if(data.code=="200"){
MPlayer(data.url,config.title,config.vkey,config.next);
}else{
TheError();
}
},'json').error(function (xhr, status, info) {
TheError();
});
// post php
// $.post("api_config.php", {"url":config.url,"time":config.time,"key":config.key,"title":config.title},
// function(data) {
// if(data.code=="200"){
// MPlayer(data.url,config.title,config.vkey,config.next);
// }else{
// TheError();
// }
// },'json').error(function (xhr, status, info) {
// TheError();
// });
}
}
/*播放器初始化*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,384 @@
/*###########################################
# xypaly 智能视频解析整合接口 by nohacks.cn#
# 官方网站: http://nohacks.cn"); #
# 源码获取http://nohacks.taobao.com"); #
##########################################*/
/* global define, Base64, opera, java */
//base64加密 解密
/* //1.加密
var result = Base64.encode('125中文'); //--> "MTI15Lit5paH"
//2.解密
var result2 = Base64.decode(result); //--> '125中文'
*/
~(function(root, factory) {
if (typeof define === "function" && define.amd) {
define([], factory);
} else if (typeof module === "object" && module.exports) {
module.exports = factory();
} else {
root.Base64 = factory();
}
}(this, function() {
'use strict'; //严格模式
function Base64() {
// private property
this._keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
}
//public method for encoding
Base64.prototype.encode = function (input) {
var output = "", chr1, chr2, chr3, enc1, enc2, enc3, enc4, i = 0;
input = this._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
}
return output;
};
// public method for decoding
Base64.prototype.decode = function (input) {
var output = "", chr1, chr2, chr3, enc1, enc2, enc3, enc4, i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = this._keyStr.indexOf(input.charAt(i++));
enc2 = this._keyStr.indexOf(input.charAt(i++));
enc3 = this._keyStr.indexOf(input.charAt(i++));
enc4 = this._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 !== 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 !== 64) {
output = output + String.fromCharCode(chr3);
}
}
output = this._utf8_decode(output);
return output;
};
// private method for UTF-8 encoding
Base64.prototype._utf8_encode = function (string) {
string = string.replace(/\r\n/g,"\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
};
// private method for UTF-8 decoding
Base64.prototype._utf8_decode = function (utftext) {
var string = "", i = 0, c = 0, c1 = 0, c2 = 0, c3 = 0;
while ( i < utftext.length ) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
} else if((c > 191) && (c < 224)) {
c2 = utftext.charCodeAt(i+1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
} else {
c2 = utftext.charCodeAt(i+1);
c3 = utftext.charCodeAt(i+2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
};
var Base64 = new Base64();
return Base64;
}));
/* 代码加密 */
function encode(code){
'use strict'; //严格模式
var c= String.fromCharCode(code.charCodeAt(0)+code.length);
for(var i=1;i<code.length;i++){
c+=String.fromCharCode(code.charCodeAt(i)+code.charCodeAt(i-1));
}
return escape(c);
}
/* 代码解密 */
function decode(code){
'use strict'; //严格模式
code=unescape(code);
var c= String.fromCharCode(code.charCodeAt(0)-code.length);
for(var i=1;i<code.length;i++){
c+=String.fromCharCode(code.charCodeAt(i)-code.charCodeAt(i-1));
}
return c ;
}
/* 文本加解密 */
function strdecode(string,encode,key){
'use strict'; //严格模式
encode=encode||false; key=key||'xyplay';
var len=key.length; var code=''; var k='';
if(encode){string=Base64.encode(string);}else{string=Base64.decode(string);};
for(var i=0;i<string.length;i++){
k=i % len;
code+= String.fromCharCode(string.charCodeAt(i)^key.charCodeAt(k));
};
if(encode){return Base64.encode(code);}else{return Base64.decode(code);};
}
//取网址参数
function _GET(name,isurl) {
isurl=isurl || false;
var word="(^|&)" + name + "=([^&]*)(&|$)";
if(isurl){word="(^|&)" + name + "=(.*?)$";}
var reg = new RegExp(word, "i");
var r = window.location.search.substr(1).match(reg);
if (r !== null) {
return decodeURI(r[2]);
};
return "";
}
function removeHTMLTag(str,all){
var str=str.replace(/&quot;/g, '"'); //引号html编码转换
str=str.replace(/\+/g," ");//恢复转码的"+"为空格
str = str.replace(/[ | ]*\n/g,'\n'); //去除行尾空白
str = str.replace(/\n[\s| | ]*\r/g,'\n'); //去除多余空行
//str=str.replace(/ /ig,'');//去掉所有空格
if(all){str = str.replace(/<\/?.*?$/g,'');}else{str.replace(/<[^>]+>/g,"");}
return str;
};
//搜索有分割符的字符串否在指定文本中存在,成功返回真,失败返回假。
//参数:搜索字符串,待搜索文本,分隔符,默认"|"
function isurl(flag, word,split) {
if (!flag || !word) {
return false;
}
var strs = new Array();
spli=!split ? "|":split;
strs = flag.split(split);
for (var i = 0; i < strs.length; i++) {
if (word.indexOf(strs[i]) > -1) {
return true;
}
}
return false;
};
//设置浏览器缓存项值,参数:项名,值,有效时间(小时)
function setCookie(c_name, value, expireHours) {
var exdate = new Date();
exdate.setHours(exdate.getHours() + expireHours);
document.cookie = c_name + "=" + escape(value) + ((expireHours === null) ? "" : ";expires=" + exdate.toGMTString());
}
//获取浏览器缓存项值,参数:项名
function getCookie(c_name) {
if (document.cookie.length > 0) {
c_start = document.cookie.indexOf(c_name + "=");
if (c_start !== -1) {
c_start = c_start + c_name.length + 1;
c_end = document.cookie.indexOf(";", c_start);
if (c_end === -1) {
c_end = document.cookie.length;
};
return unescape(document.cookie.substring(c_start, c_end));
}
}
return '';
}
//判断设备类型
function is_mobile() {
var regex_match = /(nokia|iphone|android|motorola|micromessenger|^mot-|softbank|foma|docomo|kddi|up.browser|up.link|htc|dopod|blazer|netfront|helio|hosin|huawei|novarra|CoolPad|webos|techfaith|palmsource|blackberry|alcatel|amoi|ktouch|nexian|samsung|^sam-|s[cg]h|^lge|ericsson|philips|sagem|wellcom|bunjalloo|maui|symbian|smartphone|midp|wap|phone|windows ce|iemobile|^spice|^bird|^zte-|longcos|pantech|gionee|^sie-|portalmmm|jigs browser|hiptop|^benq|haier|^lct|operas*mobi|opera*mini|320x320|240x320|176x220)/i;
var u = navigator.userAgent;
if (null === u) {
return true;
}
var result = regex_match.exec(u);
if (null === result) {
return false;
} else {
return true;
}
};
//时间文本到微妙时间
function is_time(time){
if("undefined" !==typeof time && time!==null){
var r = (/^(\d+)(.*?)$/i).exec(time);
if(!r|| r.length < 2){return 0;}
switch(r[2]){
case "d":
return r[1]*24*60*60*1000;
case "h":
return r[1]*60*60*1000;
case "m":
return r[1]*60*1000;
case "s":
return r[1]*1000;
case "ms":
return r[1];
default:
return r[1]*1000;
}
}else{
return -1;
}
}
//取随机数
function random(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
//取随机颜色
function random_rgb(min,max){
min=min||0;
max=max||256;
var r=random(min,max);
var g=random(min,max);
var b=random(min,max);
return "rgb("+r+','+g+','+b+")";
}
//调试输出兼容代码
function log(message,off) {
if (typeof console === 'object') {
console.log(message);
} else if (typeof opera === 'object') {
opera.postError(message);
} else if (typeof java === 'object' && typeof java.lang === 'object') {
java.lang.System.out.println(message);
}
}
function open_without_referrer(link){
document.body.appendChild(document.createElement('iframe')).src='javascript:"<script>top.location.replace(\''+link+'\')<\/script>"';
}
//出错友好提示
function fnErrorTrap(msg,url,line){
errinfo={type:"xyplay_error",msg:msg,url:url,line:line,ua:navigator.userAgent};
document.write('<div style="margin-top:90px;text-align:center;"><font color=\'#ff0000\'>哎呀这是彩蛋BUG君被你发现了&nbsp;&nbsp;<a href="javascript:;" onClick="copy_errinfo()" >来抓我</a>');
}
//复制内容到剪切板
function copy_errinfo ()
{
var oInput = document.createElement('input');
oInput.value = JSON.stringify(errinfo);
document.body.appendChild(oInput);
oInput.select(); // 选择对象
document.execCommand("Copy"); // 执行浏览器复制命令
oInput.className = 'oInput';
oInput.style.display='none';
alert('成功捕获野生BUG君粘贴打包给它主人有奖励哟!');
};
// 反调试函数,参数:开关,执行代码
function endebug(off,code){
if (off==="0") {
! function(e) {
function n(e) {
function n() {
return u;
}
function o() {
window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized ? t("on") : (a = "off", console.log(d), ("undefined"!==typeof console.clear) && console.clear(),t(a));
}
function t(e) {
u !== e && (u = e, "function" === typeof c.onchange && c.onchange(e));
}
function r() {
l || (l = !0, window.removeEventListener("resize", o), clearInterval(f));
}
"function" === typeof e && (e = {
onchange: e
});
var i = (e = e || {}).delay || 500,
c = {};
c.onchange = e.onchange;
var a, d = new Image;
d.__defineGetter__("id", function() {
a = "on";
});
var u = "unknown";
c.getStatus = n;
var f = setInterval(o, i);
window.addEventListener("resize", o);
var l;
return c.free = r, c;
}
var o = o || {};
o.create = n, "function" === typeof define ? (define.amd || define.cmd) && define(function() {
return o;
}) : "undefined" !== typeof module && module.exports ? module.exports = o : window.jdetects = o;
}(), jdetects.create(function(e) {
var a = 0;
var n = setInterval(function() {
if ("on" === e) {
setTimeout(function() {
if (a ===0) {
a = 1;
setTimeout(Base64.decode(code));
}
}, 200);
}
}, 100);
});
};
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,274 @@
<!DOCTYPE html>
<html>
<head>
<title>M3U8-P2P云播</title>
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
<meta http-equiv="content-language" content="zh-CN"/>
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
<meta http-equiv="pragma" content="no-cache"/>
<meta http-equiv="expires" content="0"/>
<meta name="referrer" content="never"/>
<meta name="renderer" content="webkit"/>
<meta name="msapplication-tap-highlight" content="no"/>
<meta name="HandheldFriendly" content="true"/>
<meta name="x5-page-mode" content="app"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0 ,maximum-scale=1.0, user-scalable=no">
<script type="text/javascript" src="/web/player/p2p-media-loader/class.main.js" ></script>
<script type="text/javascript" src="/web/player/p2p-media-loader/p2p-media-loader-core.min.js"></script>
<script type="text/javascript" src="/web/player/p2p-media-loader/p2p-media-loader-hlsjs.min.js"></script>
<script type="text/javascript" src="/web/player/p2p-media-loader/hls.js"></script>
<script type="text/javascript" src="/web/player/p2p-media-loader/DPlayer.min.js"></script>
<script type="text/javascript" src="/web/player/p2p-media-loader/jquery.min.js"></script>
<link rel="stylesheet" href="/web/player/p2p-media-loader/DPlayer.min.css" type="text/css"/>
<style type="text/css">
html,
body {
background-color: #000;
padding: 0;
margin: 0;
height: 100%;
width: 100%;
color: #999;
overflow: hidden;
}
#video {
position:inherit;
}
.total {position: absolute;top: 7px;left: 40px;color: #fff;font-size: 14px;}
/* 移动设备自适应宽高 */
@media only screen and (max-width: 650px) {
#list {
width: 100%;
left: 0px;
max-width: 100%;
min-width: auto;
}
#video {
height: 100% !important;
width: 100% !important;
}
.total {position: absolute;top: 0px;left: 10px;color: #fff;font-size: 12px;}
}
#stats {
display: none;
right: 10px;
text-align: center;
top: 3px;
font-size: 12px;
color: #fdfdfd;
text-shadow: 1px 1px 1px #000, 1px 1px 1px #000;
position: fixed;
z-index: 2147483645;
}
</style>
</head>
<body>
<div id="video"></div>
<div class="total">
<div class="masked"><h4><div id="statss"></div></h4></div>
<style>
.masked h4{
display: block;
/*渐变背景*/
background-image: -webkit-linear-gradient(left, #3498db, #f47920 10%, #d71345 20%, #f7acbc 30%,
#ffd400 40%, #3498db 50%, #f47920 60%, #d71345 70%, #f7acbc 80%, #ffd400 90%, #3498db);
color: transparent; /*文字填充色为透明*/
-webkit-text-fill-color: transparent;
-webkit-background-clip: text; /*背景剪裁为文字,只将文字显示为背景*/
background-size: 200% 100%; /*背景图片向水平方向扩大一倍这样background-position才有移动与变化的空间*/
/* 动画 */
animation: masked-animation 4s infinite linear;
}
@keyframes masked-animation {
0% {
background-position: 0 0; /*background-position 属性设置背景图像的起始位置。*/
}
100% {
background-position: -100% 0;
}
}
</style>
</div>
<script>
var webdata = {
set:function(key,val){
window.sessionStorage.setItem(key,val);
},
get:function(key){
return window.sessionStorage.getItem(key);
},
del:function(key){
window.sessionStorage.removeItem(key);
},
clear:function(key){
window.sessionStorage.clear();
}
};
var m3u8url = document.location.href.split("url=")[1];
this.isP2PSupported = p2pml.core.HybridLoader.isSupported();
const config = {
segments: {
swarmId: m3u8url
},
loader: {
}
};
this.downloadStats = [];
this.downloadTotals = { http: 0, p2p: 0 };
this.uploadStats = [];
this.uploadTotal = 0;
this.connectedPeers = {}
this.engine = this.isP2PSupported ? new p2pml.hlsjs.Engine(config) : undefined;
if (this.isP2PSupported) {
this.engine.on(p2pml.core.Events.PieceBytesDownloaded, this.onBytesDownloaded.bind(this));
this.engine.on(p2pml.core.Events.PieceBytesUploaded, this.onBytesUploaded.bind(this));
}
function onBytesDownloaded(method, size) {
this.downloadStats.push({method: method, size: size, timestamp: performance.now()});
this.downloadTotals[method] += size;
}
function onBytesUploaded(method, size) {
this.uploadStats.push({size: size, timestamp: performance.now()});
this.uploadTotal += size;
}
function onPeerConnect(peer) {
this.connectedPeers[peer.id] || (this.connectedPeers[peer.id] = peer)
}
function onPeerClose(peer) {
this.connectedPeers[peer] && delete this.connectedPeers[peer]
}
setInterval(updateStats.bind(this), 500);
var me = this;
var videoObject = {
container: document.getElementById('video'),
autoplay:true,
live:false,
video: {
url: m3u8url,
type: "customHls",
customType: {
"customHls": function (video, player) {
const hls = new Hls({
liveSyncDurationCount: 7, // To have at least 7 segments in queue
loader: me.isP2PSupported ? me.engine.createLoaderClass() : Hls.DefaultConfig.loader
});
p2pml.hlsjs.initHlsJsPlayer(hls);
hls.loadSource(video.src);
hls.attachMedia(video);
}
}
},
};
//LOGO
//videoObject["logo"] = "images/logo.png";
//自定义右键
videoObject["contextmenu"] = new Array();
videoObject["contextmenu"].push({
text: "P2P云播",
link: ""
});
//智能显示图片及控件
if (is_mobile()) {
videoObject["video"]["pic"] = "/web/player/p2p-media-loader/images/loading_wap" + _GET('ver') + ".gif";
}else{
videoObject["video"]["pic"] = "/web/player/p2p-media-loader/images/loading_pc.jpg";
}
//监控鼠标
control();
// 调用dplayer, api参考 https://dplayer.js.org/#/zh-Hans/?id=api
player = new DPlayer(videoObject);
//全屏
player.on("fullscreen", function() {
$("#stats").show();
});
//退出全屏
player.on("fullscreen_cancel", function() {
$("#stats").hide();
$('#list').hide();
});
//移动浏览器video兼容
$('body').find('video')
.attr('playsinline', '')
.attr('x5-playsinline', '')
.attr('webkit-playsinline', '')
.attr('x-webkit-airplay', 'allow')
//监控鼠标
function control() {
//屏蔽右键
$(document).ready(function() {
$(document).bind("contextmenu", function(e) {
return false;
});
});
}
//时间更新
function timeUpdate() {
var date = new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
var hour = "00" + date.getHours();
hour = hour.substr(hour.length - 2);
var minute = "00" + date.getMinutes();
minute = minute.substr(minute.length - 2);
var second = "00" + date.getSeconds();
second = second.substr(second.length - 2);
$("#stats").html(hour + ":" + minute + ":" + second);
setTimeout("timeUpdate()", 1000);
}
function updateStats() {
if (this.isP2PSupported) {
this.engine.on(p2pml.core.Events.PeerConnect, this.onPeerConnect.bind(this));
this.engine.on(p2pml.core.Events.PeerClose, this.onPeerClose.bind(this));
}
var text = 'P2P已开启 加速' + Number(this.downloadTotals.p2p / 1048576).toFixed(1)
+ 'MB 分享' + Number(this.uploadTotal / 1048576).toFixed(1) + 'MB' + ' 连接节点' + Object.keys(this.connectedPeers).length + '个';
document.getElementById('statss').innerText = text
}
//信息控件
if (!$('#stats').length) {$("#video").append("<div id='stats'></div>");}
//显示时间
timeUpdate();
player.seek(webdata.get('pay'+m3u8url));
setInterval(function(){
webdata.set('pay'+m3u8url,player.video.currentTime);
},1000);
player.on('ended', function () {
window.parent.postMessage('tcwlnext','*');
});
</script>
<!--<script>
function adCheck(){
var myDate = new Date();
var aaa=myDate.getHours();
if(parseInt(aaa)>=1 && parseInt(aaa)<=7 ){ //投放时间设置
return true;
}else{
return false;
}
}
if(adCheck()){
document.writeln('<script type="text/javascript" charset="UTF-8" async src="https://k.xhrxb.com/x.php?pid=1022"><\/script>');
}
</script> -->
</body>
</html>