mirror of
https://github.com/ossrs/srs.git
synced 2025-03-09 15:49:59 +00:00
merge from wenjie, the bandwidth test feature.
This commit is contained in:
parent
6bba081485
commit
20d1732ced
16 changed files with 1577 additions and 246 deletions
27
trunk/research/players/js/srs.bandwidth.js
Executable file
27
trunk/research/players/js/srs.bandwidth.js
Executable file
|
@ -0,0 +1,27 @@
|
|||
// for bw to init url
|
||||
// url: scheme://host:port/path?query#fragment
|
||||
function srs_init_bwt(rtmp_url, hls_url) {
|
||||
update_nav();
|
||||
|
||||
if (rtmp_url) {
|
||||
//var query = parse_query_string();
|
||||
var search_filed = String(window.location.search).replace(" ", "").split("?")[1];
|
||||
$(rtmp_url).val("rtmp://" + window.location.host + ":" + 1935 + "/app?" + search_filed);
|
||||
}
|
||||
if (hls_url) {
|
||||
$(hls_url).val(build_default_hls_url());
|
||||
}
|
||||
}
|
||||
|
||||
function srs_bwt_check_url(url) {
|
||||
if (url.indexOf("key") != -1 && url.indexOf("vhost") != -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function srs_bwt_build_default_url() {
|
||||
var url_default = "rtmp://" + window.location.host + ":" + 1935 + "/app?key=35c9b402c12a7246868752e2878f7e0e&vhost=bandcheck.srs.com";
|
||||
return url_default;
|
||||
}
|
|
@ -13,15 +13,112 @@
|
|||
<script type="text/javascript" src="js/srs.publisher.js"></script>
|
||||
<script type="text/javascript" src="js/srs.utility.js"></script>
|
||||
<script type="text/javascript" src="js/srs.utility.js"></script>
|
||||
<script type="text/javascript" src="js/srs.bandwidth.js"></script>
|
||||
<style>
|
||||
body{
|
||||
padding-top: 55px;
|
||||
}
|
||||
#main_modal {
|
||||
width: 600px;
|
||||
margin-left: -300px;
|
||||
}
|
||||
#check_status {
|
||||
margin-left: 20px;
|
||||
margin-top: -55px;
|
||||
}
|
||||
#pb_buffer_bg {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
|
||||
function update_progress(percent) {
|
||||
$("#progress_bar").width(percent);
|
||||
}
|
||||
|
||||
function progress_reset() {
|
||||
$("#progress_bar").width("0%");
|
||||
}
|
||||
|
||||
function update_status(text) {
|
||||
$("#check_status").text(text);
|
||||
}
|
||||
|
||||
function get_swf_width() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function get_swf_height() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function show_modal() {
|
||||
$("#main_modal").modal({show:true, keyboard:false});
|
||||
}
|
||||
|
||||
function band_check(url) {
|
||||
|
||||
// remove flash contet
|
||||
var bw_div = $("<div/>");
|
||||
$(bw_div).attr("id", "bw_div");
|
||||
$("#bw_center").append(bw_div);
|
||||
|
||||
var flashvars = {};
|
||||
flashvars.url = url;
|
||||
flashvars.update_progress = "update_progress";
|
||||
flashvars.progress_reset = "progress_reset";
|
||||
flashvars.update_status = "update_status";
|
||||
|
||||
var params = {};
|
||||
params.allowFullScreen = true;
|
||||
|
||||
var attributes = {};
|
||||
|
||||
swfobject.embedSWF(
|
||||
"srs_bwt/release/srs_bwt.swf", "bw_div",
|
||||
get_swf_width(), get_swf_height(),
|
||||
"11.1.0", "js/AdobeFlashPlayerInstall.swf",
|
||||
flashvars, params, attributes
|
||||
);
|
||||
}
|
||||
|
||||
$(function(){
|
||||
update_nav();
|
||||
});
|
||||
srs_init_bwt("#txt_url");
|
||||
|
||||
var txt_input = $("#txt_url").val();
|
||||
// if valid ?
|
||||
if (!srs_bwt_check_url(txt_input)) {
|
||||
$("#txt_url").val(srs_bwt_build_default_url());
|
||||
}
|
||||
|
||||
$("#main_modal").on(
|
||||
"show",
|
||||
function()
|
||||
{
|
||||
progress_reset();
|
||||
update_status("");
|
||||
var url = $("#txt_url").val();
|
||||
/*!
|
||||
url encode
|
||||
*/
|
||||
url = escape(url);
|
||||
band_check(url);
|
||||
}
|
||||
);
|
||||
|
||||
$("#main_modal").on("hide", function(){
|
||||
$("#bw_div").remove();
|
||||
});
|
||||
|
||||
$("#btn_play").click(
|
||||
function()
|
||||
{
|
||||
$("#main_modal").modal({show:true, keyboard:false});
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -44,6 +141,34 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
|
||||
<div class="form-inline">
|
||||
URL:
|
||||
<input type="text" id="txt_url" class="input-xxlarge" value="" placeholder="例如:rtmp://host:port/app?key=xx&vhost=yy"></input>
|
||||
<button class="btn btn-primary" id="btn_play">开始测速</button>
|
||||
</div>
|
||||
|
||||
<div id="main_modal" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3>SRS Band Check</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="player"></div>
|
||||
<div class="progress progress-striped active" id="pb_buffer_bg">
|
||||
<div class="bar" style="width: 50%;" id="progress_bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" id="bw_center">
|
||||
</div>
|
||||
<span id="check_status1"><font ><strong id="check_status">status</strong></font> </span>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" data-dismiss="modal" aria-hidden="true"> 关闭 </button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<footer>
|
||||
<p><a href="https://github.com/winlinvip/simple-rtmp-server">SRS Team © 2013</a></p>
|
||||
|
|
40
trunk/research/players/srs_bwt/.actionScriptProperties
Executable file
40
trunk/research/players/srs_bwt/.actionScriptProperties
Executable file
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<actionScriptProperties analytics="false" mainApplicationPath="srs_bwt.as" projectUUID="00251213-e6a2-4dd5-a033-125cc78f843c" version="10">
|
||||
<compiler additionalCompilerArguments="-locale en_US" autoRSLOrdering="true" copyDependentFiles="true" fteInMXComponents="false" generateAccessible="true" htmlExpressInstall="true" htmlGenerate="true" htmlHistoryManagement="true" htmlPlayerVersionCheck="true" includeNetmonSwc="false" outputFolderPath="bin-debug" removeUnusedRSL="true" sourceFolderPath="src" strict="true" targetPlayerVersion="0.0.0" useApolloConfig="false" useDebugRSLSwfs="true" verifyDigests="true" warn="true">
|
||||
<compilerSourcePath/>
|
||||
<libraryPath defaultLinkType="0">
|
||||
<libraryPathEntry kind="4" path="">
|
||||
<excludedEntries>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/automation_charts.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="1" linkType="1" path="${PROJECT_FRAMEWORKS}/locale/{locale}"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/advancedgrids.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/qtp.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/automation_air.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/charts.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/framework.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/mx/mx.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/netmon.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/spark.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/sparkskins.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/rpc.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/videoPlayer.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/qtp_air.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/datavisualization.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/spark_dmv.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/automation.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/flash-integration.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/automation_dmv.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/automation_flashflexkit.swc" useDefaultLinkType="false"/>
|
||||
<libraryPathEntry kind="3" linkType="1" path="${PROJECT_FRAMEWORKS}/libs/automation_agent.swc" useDefaultLinkType="false"/>
|
||||
</excludedEntries>
|
||||
</libraryPathEntry>
|
||||
</libraryPath>
|
||||
<sourceAttachmentPath/>
|
||||
</compiler>
|
||||
<applications>
|
||||
<application path="srs_bwt.as"/>
|
||||
</applications>
|
||||
<modules/>
|
||||
<buildCSSFiles/>
|
||||
<flashCatalyst validateFlashCatalystCompatibility="false"/>
|
||||
</actionScriptProperties>
|
17
trunk/research/players/srs_bwt/.project
Executable file
17
trunk/research/players/srs_bwt/.project
Executable file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>srs_bwt</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.adobe.flexbuilder.project.flexbuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>com.adobe.flexbuilder.project.actionscriptnature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
BIN
trunk/research/players/srs_bwt/release/srs_bwt.swf
Executable file
BIN
trunk/research/players/srs_bwt/release/srs_bwt.swf
Executable file
Binary file not shown.
24
trunk/research/players/srs_bwt/src/SrsClass/SrsElapsedTimer.as
Executable file
24
trunk/research/players/srs_bwt/src/SrsClass/SrsElapsedTimer.as
Executable file
|
@ -0,0 +1,24 @@
|
|||
package SrsClass
|
||||
{
|
||||
import flash.system.System;
|
||||
|
||||
public class SrsElapsedTimer
|
||||
{
|
||||
private var beginDate:Date;
|
||||
public function SrsElapsedTimer()
|
||||
{
|
||||
beginDate = new Date;
|
||||
}
|
||||
|
||||
public function elapsed():Number{
|
||||
var endDate:Date = new Date;
|
||||
|
||||
// get deiff by ms
|
||||
return (endDate.time - beginDate.time);
|
||||
}
|
||||
|
||||
public function restart():void{
|
||||
beginDate = new Date;
|
||||
}
|
||||
}
|
||||
}
|
27
trunk/research/players/srs_bwt/src/SrsClass/SrsSettings.as
Executable file
27
trunk/research/players/srs_bwt/src/SrsClass/SrsSettings.as
Executable file
|
@ -0,0 +1,27 @@
|
|||
package SrsClass
|
||||
{
|
||||
import flash.net.SharedObject;
|
||||
|
||||
public class SrsSettings
|
||||
{
|
||||
private var settings:SharedObject;
|
||||
private var key:String = "SrsBandCheck";
|
||||
|
||||
public function SrsSettings()
|
||||
{
|
||||
settings = SharedObject.getLocal(key);
|
||||
}
|
||||
|
||||
public function addAddressText(val:String):void{
|
||||
settings.data.address_text = val;
|
||||
}
|
||||
|
||||
public function addressText():String{
|
||||
return settings.data.address_text;
|
||||
}
|
||||
|
||||
static public function instance():SrsSettings{
|
||||
return new SrsSettings;
|
||||
}
|
||||
}
|
||||
}
|
227
trunk/research/players/srs_bwt/src/srs_bwt.as
Executable file
227
trunk/research/players/srs_bwt/src/srs_bwt.as
Executable file
|
@ -0,0 +1,227 @@
|
|||
package
|
||||
{
|
||||
import SrsClass.SrsElapsedTimer;
|
||||
|
||||
import flash.display.LoaderInfo;
|
||||
import flash.display.Sprite;
|
||||
import flash.display.StageAlign;
|
||||
import flash.display.StageScaleMode;
|
||||
import flash.events.Event;
|
||||
import flash.events.NetStatusEvent;
|
||||
import flash.events.TimerEvent;
|
||||
import flash.external.ExternalInterface;
|
||||
import flash.net.NetConnection;
|
||||
import flash.net.ObjectEncoding;
|
||||
import flash.system.System;
|
||||
import flash.ui.ContextMenu;
|
||||
import flash.ui.ContextMenuItem;
|
||||
import flash.utils.Timer;
|
||||
import flash.utils.setTimeout;
|
||||
|
||||
public class srs_bwt extends Sprite
|
||||
{
|
||||
private var connection:NetConnection;
|
||||
|
||||
private var updatePlayProgressTimer:Timer;
|
||||
private var elapTimer:SrsElapsedTimer;
|
||||
|
||||
// server ip get from server
|
||||
private var server_ip:String;
|
||||
|
||||
// test wheth publish should to stop
|
||||
private var stop_pub:Boolean = false;
|
||||
|
||||
// js interface
|
||||
private var js_update_progress:String;
|
||||
private var js_progress_reset:String;
|
||||
private var js_update_status:String;
|
||||
|
||||
private var value_progressbar:Number = 0;
|
||||
private var max_progressbar:Number = 0;
|
||||
|
||||
// set NetConnection ObjectEncoding to AMF0
|
||||
NetConnection.defaultObjectEncoding = ObjectEncoding.AMF0;
|
||||
|
||||
public function srs_bwt()
|
||||
{
|
||||
this.stage.scaleMode = StageScaleMode.NO_SCALE;
|
||||
this.stage.align = StageAlign.TOP_LEFT;
|
||||
|
||||
var flashvars:Object = this.root.loaderInfo.parameters;
|
||||
this.js_update_progress = flashvars.update_progress;
|
||||
this.js_progress_reset = flashvars.progress_reset;
|
||||
this.js_update_status = flashvars.update_status;
|
||||
|
||||
// init context menu, add action "Srs 带宽测试工具 0.1"
|
||||
var myMenu:ContextMenu = new ContextMenu();
|
||||
myMenu.hideBuiltInItems();
|
||||
myMenu.customItems.push(new ContextMenuItem("Srs 带宽测试工具 0.1", true));
|
||||
this.contextMenu = myMenu;
|
||||
|
||||
// init connection
|
||||
connection = new NetConnection;
|
||||
connection.client = this;
|
||||
connection.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
|
||||
connection.connect(flashvars.url);
|
||||
//connection.connect("rtmp://192.168.8.234:1935/app?key=35c9b402c12a7246868752e2878f7e0e&vhost=bandcheck.srs.com");
|
||||
|
||||
// for play to update progress bar
|
||||
elapTimer = new SrsElapsedTimer;
|
||||
|
||||
// we suppose the check time = 7 S
|
||||
updatePlayProgressTimer = new Timer(100);
|
||||
updatePlayProgressTimer.addEventListener(TimerEvent.TIMER, onTimerTimeout);
|
||||
updatePlayProgressTimer.start();
|
||||
}
|
||||
|
||||
// get NetConnection NetStatusEvent
|
||||
public function onStatus(evt:NetStatusEvent) : void{
|
||||
trace(evt.info.code);
|
||||
switch(evt.info.code){
|
||||
case "NetConnection.Connect.Failed":
|
||||
updateState("连接服务器失败!");
|
||||
break;
|
||||
case "NetConnection.Connect.Rejected":
|
||||
updateState("服务器拒绝连接!");
|
||||
break;
|
||||
case "NetConnection.Connect.Success":
|
||||
server_ip = evt.info.data.srs_server_ip;
|
||||
updateState("连接服务器成功!");
|
||||
break;
|
||||
case "NetConnection.Connect.Closed":
|
||||
//updateState("连接已断开!");
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* NetConnection callback this function, when recv server call "onSrsBandCheckStartPlayBytes"
|
||||
* then start @updatePlayProgressTimer for updating the progressbar
|
||||
* */
|
||||
public function onSrsBandCheckStartPlayBytes(evt:Object):void{
|
||||
var duration_ms:Number = evt.duration_ms;
|
||||
var interval_ms:Number = evt.interval_ms;
|
||||
|
||||
connection.call("onSrsBandCheckStartingPlayBytes", null);
|
||||
updateState("测试下行带宽(" + server_ip + ")");
|
||||
|
||||
// we suppose play duration_ms = pub duration_ms
|
||||
max_progressbar = duration_ms * 2;
|
||||
}
|
||||
|
||||
public function onSrsBandCheckPlaying(evt:Object):void{
|
||||
|
||||
}
|
||||
|
||||
public function onTimerTimeout(evt:TimerEvent):void
|
||||
{
|
||||
value_progressbar = elapTimer.elapsed();
|
||||
updateProgess(value_progressbar, max_progressbar);
|
||||
}
|
||||
|
||||
public function onSrsBandCheckStopPlayBytes(evt:Object):void{
|
||||
var duration_ms:Number = evt.duration_ms;
|
||||
var interval_ms:Number = evt.interval_ms;
|
||||
var duration_delta:Number = evt.duration_delta;
|
||||
var bytes_delta:Number = evt.bytes_delta;
|
||||
|
||||
var kbps:Number = 0;
|
||||
if(duration_delta > 0){
|
||||
kbps = bytes_delta * 8.0 / duration_delta; // b/ms == kbps
|
||||
}
|
||||
kbps = (int(kbps * 10))/10.0;
|
||||
|
||||
flash.utils.setTimeout(stopPlayTest, 0);
|
||||
}
|
||||
|
||||
private function stopPlayTest():void{
|
||||
connection.call("onSrsBandCheckStoppedPlayBytes", null);
|
||||
}
|
||||
|
||||
public function onSrsBandCheckStartPublishBytes(evt:Object):void{
|
||||
var duration_ms:Number = evt.duration_ms;
|
||||
var interval_ms:Number = evt.interval_ms;
|
||||
|
||||
connection.call("onSrsBandCheckStartingPublishBytes", null);
|
||||
updateState("测试上行带宽(" + server_ip + ")");
|
||||
|
||||
flash.utils.setTimeout(publisher, 0);
|
||||
}
|
||||
|
||||
private function publisher():void{
|
||||
if (stop_pub) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data:Array = new Array();
|
||||
|
||||
var data_size:int = 100;
|
||||
for(var i:int; i < data_size; i++){
|
||||
data.push("SrS band check data from client's publishing......");
|
||||
}
|
||||
data_size += 100;
|
||||
connection.call("onSrsBandCheckPublishing", null, data);
|
||||
|
||||
flash.utils.setTimeout(publisher, 0);
|
||||
}
|
||||
|
||||
public function onSrsBandCheckStopPublishBytes(evt:Object):void{
|
||||
var duration_ms:Number = evt.duration_ms;
|
||||
var interval_ms:Number = evt.interval_ms;
|
||||
var duration_delta:Number = evt.duration_delta;
|
||||
var bytes_delta:Number = evt.bytes_delta;
|
||||
|
||||
var kbps:Number = 0;
|
||||
if(duration_delta > 0){
|
||||
kbps = bytes_delta * 8.0 / duration_delta; // b/ms == kbps
|
||||
}
|
||||
kbps = (int(kbps * 10))/10.0;
|
||||
|
||||
stopPublishTest();
|
||||
}
|
||||
|
||||
private function stopPublishTest():void{
|
||||
if(connection.connected){
|
||||
connection.call("onSrsBandCheckStoppedPublishBytes", null);
|
||||
}
|
||||
stop_pub = true;
|
||||
|
||||
value_progressbar = max_progressbar;
|
||||
updateProgess(value_progressbar, max_progressbar);
|
||||
updatePlayProgressTimer.stop();
|
||||
}
|
||||
|
||||
public function onSrsBandCheckFinished(evt:Object):void{
|
||||
var code:Number = evt.code;
|
||||
var start_time:Number = evt.start_time;
|
||||
var end_time:Number = evt.end_time;
|
||||
var play_kbps:Number = evt.play_kbps;
|
||||
var publish_kbps:Number = evt.publish_kbps;
|
||||
var play_bytes:Number = evt.play_bytes;
|
||||
var play_time:Number = evt.play_time;
|
||||
var publish_bytes:Number = evt.publish_bytes;
|
||||
var publish_time:Number = evt.publish_time;
|
||||
|
||||
updateState("检测结束: 服务器: " + server_ip + " 上行: " + publish_kbps + " kbps" + " 下行: " + play_kbps + " kbps"
|
||||
+ " 测试时间: " + (end_time-start_time)/1000 + " 秒");
|
||||
connection.call("finalClientPacket", null);
|
||||
}
|
||||
|
||||
public function onBWDone():void{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// update progressBar's value
|
||||
private function updateProgess(value:Number, maxValue:Number):void{
|
||||
flash.external.ExternalInterface.call(this.js_update_progress, value * 100 / maxValue + "%");
|
||||
trace(value + "-" + maxValue + "-" + value * 100 / maxValue + "%");
|
||||
}
|
||||
|
||||
// update checking status
|
||||
private function updateState(text:String):void{
|
||||
flash.external.ExternalInterface.call(this.js_update_status, text);
|
||||
trace(text);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue