first
This commit is contained in:
1 parent
af2f967059
commit
754c24615c
166 files changed
+81865
-2
No files matched your search
@@ -0,0 +1,99 @@
|
||||
{{template "head.tmpl" .}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">Add stream</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Add stream</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row" id="streams-form-wrapper">
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Main channel<small> parameters</small></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="stream-form main-form">
|
||||
<div class="form-group">
|
||||
<label for="exampleInputEmail1">Stream name</label>
|
||||
<input type="text" class="form-control" name="stream-name" placeholder="Enter stream name" id="stream-name">
|
||||
<small class="form-text text-muted">You can choose any name for the stream, for example "My room" or "Happy sausage"</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="exampleInputPassword1">Stream url</label>
|
||||
<input type="text" name="stream-url" class="form-control" placeholder="Enter stream url">
|
||||
<small class="form-text text-muted">Enter rtsp address as instructed by your camera. Look like <code>rtsp://<ip>:<port>/path </code> </small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputStatus">Stream type</label>
|
||||
<select class="form-control custom-select" name="stream-ondemand">
|
||||
<option selected disabled><small>Select One</small></option>
|
||||
<option value="1">On demand only</option>
|
||||
<option value="0">Persistent connection</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">On persistent connection, the server get data from the camera continuously. On demand, the server get data from the camera only when you click play button </small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" name="debug" id="debug-switch">
|
||||
<label class="custom-control-label" for="debug-switch">Enable debug</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Select this options if you want get more data about the stream </small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<button type="button" onclick="addChannel()" class="btn btn-secondary">Add channel</button>
|
||||
<button type="button" onclick="addStreamSubmit()" class="btn btn-primary">Save stream</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{template "foot.tmpl" .}}
|
||||
|
||||
<script>
|
||||
function addStreamSubmit(){
|
||||
var params = {
|
||||
uuid:randomUuid(),
|
||||
name:$('#stream-name').val(),
|
||||
channels:{}
|
||||
}
|
||||
|
||||
$('.stream-form').each(function(i){
|
||||
params.channels[i]={
|
||||
url:$(this).find('input[name="stream-url"]').val(),
|
||||
on_demand:Boolean(parseInt($(this).find('select[name="stream-ondemand"]').val())),
|
||||
debug:Boolean($(this).find('input[name="debug"]').prop('checked')),
|
||||
}
|
||||
if(params.channels[i].on_demand==null){
|
||||
params.channels[i].on_demand==true;
|
||||
}
|
||||
});
|
||||
goRequest('add', params.uuid, params);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,32 @@
|
||||
{{template "head.tmpl" .}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">Documentation</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Documentation</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p>
|
||||
<a href="https://github.com/deepch/RTSPtoWeb/tree/master/docs/api.md">API documentation</a> is available in the GitHub repository.
|
||||
</p>
|
||||
<p>
|
||||
See the project <a href="https://github.com/deepch/RTSPtoWeb#readme">README</a> for installation and configuration instructions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "foot.tmpl" .}}
|
||||
@@ -0,0 +1,151 @@
|
||||
{{template "head.tmpl" .}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">Edit stream</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Edit stream</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
</div>
|
||||
<input type="hidden" value="{{.uuid}}" id="uuid" />
|
||||
{{ $stream:= (index .streams .uuid)}}
|
||||
{{ $mainChannel := (index $stream.Channels "0") }}
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row" id="streams-form-wrapper">
|
||||
<div class="col-md-12">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Parameters<small> main</small></h3>
|
||||
</div>
|
||||
<form class="stream-form main-form">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="exampleInputEmail1">Stream name</label>
|
||||
<input type="text" class="form-control" name="stream-name" placeholder="Enter stream name" value="{{$stream.Name}}" id="stream-name">
|
||||
<small class="form-text text-muted">You can choose any name for the stream, for example "My room" or "Happy sausage"</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="exampleInputPassword1">Stream url</label>
|
||||
<input type="text" name="stream-url" class="form-control" placeholder="Enter stream url" value="{{$mainChannel.URL}}">
|
||||
<small class="form-text text-muted">Enter rtsp address as instructed by your camera. Look like <code>rtsp://<ip>:<port>/path </code></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputStatus">Stream type</label>
|
||||
<select class="form-control custom-select" name="stream-ondemand">
|
||||
<option selected disabled><small>Select One</small></option>
|
||||
<option value="1"
|
||||
{{ if eq $mainChannel.OnDemand true}}
|
||||
selected
|
||||
{{ end }}>On demand only</option>
|
||||
<option value="0" {{ if eq $mainChannel.OnDemand false}}
|
||||
selected
|
||||
{{ end }}>Persistent connection</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">On persistent connection, the server get data from the camera continuously. On demand, the server get data from the camera only when you click play button </small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" name="debug" id="debug-switch" {{ if eq $mainChannel.Debug true}}
|
||||
checked
|
||||
{{ end }}>
|
||||
<label class="custom-control-label" for="debug-switch">Enable debug</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Select this options if you want get more data about the stream </small>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{ if gt (len $stream.Channels) 1}}
|
||||
{{ range $key, $value := $stream.Channels }}
|
||||
{{ if ne $key "0"}}
|
||||
<div class="col-12">
|
||||
<div class="card card-secondary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Sub channel<small> parameters</small></h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-tool" onclick="removeChannelDiv(this)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="stream-form">
|
||||
<div class="form-group">
|
||||
<label for="exampleInputPassword1">Substream url</label>
|
||||
<input type="text" name="stream-url" class="form-control" placeholder="Enter stream url" value="{{$value.URL}}" >
|
||||
<small class="form-text text-muted">Enter rtsp address as instructed by your camera. Look like <code>rtsp://<ip>:<port>/path </code> </small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
<label for="inputStatus">Substream type</label>
|
||||
<select class="form-control custom-select" name="stream-ondemand">
|
||||
<option selected disabled><small>Select One</small></option>
|
||||
<option value="1"
|
||||
{{ if eq $value.OnDemand true}}
|
||||
selected
|
||||
{{ end }}>On demand only</option>
|
||||
<option value="0"
|
||||
{{ if eq $value.OnDemand false}}
|
||||
selected
|
||||
{{ end }}>Persistent connection</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">On persistent connection, the server get data from the camera continuously. On demand, the server get data from the camera only when you click play button </small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" name="debug" id="substream-debug-switch-{{$key}}" {{ if eq $value.Debug true}}
|
||||
checked
|
||||
{{ end }}>
|
||||
<label class="custom-control-label" for="substream-debug-switch-{{$key}}">Enable debug</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Select this options if you want get more data about the stream </small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<button type="button" onclick="addChannel()" class="btn btn-secondary">Add channel</button>
|
||||
<button type="button" onclick="editStreamSubmit()" class="btn btn-primary">Save stream</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{template "foot.tmpl" .}}
|
||||
|
||||
<script>
|
||||
|
||||
function editStreamSubmit(){
|
||||
var params = {
|
||||
uuid:$('#uuid').val(),
|
||||
name:$('#stream-name').val(),
|
||||
channels:{}
|
||||
}
|
||||
|
||||
$('.stream-form').each(function(i){
|
||||
params.channels[i]={
|
||||
url:$(this).find('input[name="stream-url"]').val(),
|
||||
on_demand:Boolean(parseInt($(this).find('select[name="stream-ondemand"]').val())),
|
||||
debug:Boolean($(this).find('input[name="debug"]').prop('checked')),
|
||||
}
|
||||
if(params.channels[i].on_demand==null){
|
||||
params.channels[i].on_demand==true;
|
||||
}
|
||||
});
|
||||
goRequest('edit', params.uuid, params);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
</div><!-- content-wrapper -->
|
||||
|
||||
<!-- Control Sidebar -->
|
||||
<aside class="control-sidebar control-sidebar-dark">
|
||||
<!-- Control sidebar content goes here -->
|
||||
<div class="p-3">
|
||||
<h5>Default player</h5>
|
||||
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="radio" name="defaultPlayer" value="hls">
|
||||
HLS
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="radio" name="defaultPlayer" checked value="mse">
|
||||
MSE
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="radio" name="defaultPlayer" value="webrtc">
|
||||
WebRTC
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
<!-- /.control-sidebar -->
|
||||
|
||||
<!-- Main Footer -->
|
||||
<footer class="main-footer">
|
||||
<!-- To the right -->
|
||||
<div class="float-right d-none d-sm-inline">
|
||||
<b>Version: </b> {{ .version }}
|
||||
</div>
|
||||
<!-- Default to the left -->
|
||||
<strong>Hungry Night Team.</strong> <a href="https://github.com/deepch/RTSPtoWeb">Follow us on GitHub</a>
|
||||
</footer>
|
||||
</div>
|
||||
<!-- ./wrapper -->
|
||||
<script>
|
||||
let streams = {{ .streams }};
|
||||
</script>
|
||||
<script src="/../static/plugins/jquery/jquery.min.js"></script>
|
||||
<script src="/../static/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/../static/js/adminlte.min.js"></script>
|
||||
<script src="/../static/plugins/sweetalert2/sweetalert2.min.js"></script>
|
||||
<script src="/../static/js/index.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,981 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
|
||||
<title>RTSPtoWEB</title>
|
||||
<link rel="stylesheet" href="/../static/plugins/fontawesome-free/css/all.min.css">
|
||||
<link rel="stylesheet" href="/../static/css/adminlte.min.css">
|
||||
<link rel="stylesheet" href="/../static/plugins/sweetalert2/sweetalert2.min.css">
|
||||
<link rel="stylesheet" href="/../static/css/index.css">
|
||||
<link rel="stylesheet" href="/../static/css/fullmulti.css">
|
||||
<link href="/../static/css/google-fonts.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="hold-transition layout-top-nav img-background">
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
<!-- Navbar -->
|
||||
<nav class="main-header navbar navbar-expand-md navbar-light navbar-dark">
|
||||
<div class="container-fluid">
|
||||
|
||||
|
||||
<button class="navbar-toggler order-1" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse order-3" id="navbarCollapse">
|
||||
<ul class="order-1 order-md-3 navbar-nav navbar-no-expand ml-auto">
|
||||
{{ if .query.controls}}
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="nav-link dropdown-toggle"><i class="fas fa-photo-video"></i></a>
|
||||
<div class="dropdown-menu dropdown-menu-right custom-dropdown">
|
||||
<div class="custom-dropdown-item with-img" onclick="changeBackground('back')">
|
||||
<img src="/../static/img/back.jpg" />
|
||||
</div>
|
||||
<div class="custom-dropdown-item with-img" onclick="changeBackground('red')">
|
||||
<img src="/../static/img/red.jpg" />
|
||||
</div>
|
||||
<div class="custom-dropdown-item with-img" onclick="changeBackground('green')">
|
||||
<img src="/../static/img/green.jpg" />
|
||||
</div>
|
||||
<div class="custom-dropdown-item with-img" onclick="changeBackground('white')">
|
||||
<img src="/../static/img/white.jpg" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
<li class="nav-item dropdown">
|
||||
<a id="defaultGrid" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="nav-link dropdown-toggle"><i class="fas fa-th-large"></i></a>
|
||||
<div aria-labelledby="defaultGrid" class="dropdown-menu dropdown-menu-right custom-dropdown">
|
||||
<div class="custom-dropdown-item grid-maker" grid="1" onclick="gridMaker(1)">
|
||||
1 x 1
|
||||
</div>
|
||||
<div class="custom-dropdown-item grid-maker" grid="4" onclick="gridMaker(4)">
|
||||
2 x 2
|
||||
</div>
|
||||
<div class="custom-dropdown-item grid-maker" grid="6" onclick="gridMaker(6)">
|
||||
3 x 2
|
||||
</div>
|
||||
<div class="custom-dropdown-item grid-maker" grid="9" onclick="gridMaker(9)">
|
||||
3 x 3
|
||||
</div>
|
||||
<div class="custom-dropdown-item grid-maker" grid="12" onclick="gridMaker(12)">
|
||||
4 x 3
|
||||
</div>
|
||||
<div class="custom-dropdown-item grid-maker" grid="16" onclick="gridMaker(16)">
|
||||
4 x 4
|
||||
</div>
|
||||
<div class="custom-dropdown-item grid-maker" grid="25" onclick="gridMaker(25)">
|
||||
5 x 5
|
||||
</div>
|
||||
<div class="custom-dropdown-item grid-maker" grid="36" onclick="gridMaker(36)">
|
||||
6 x 6
|
||||
</div>
|
||||
<div class="custom-dropdown-item grid-maker" grid="49" onclick="gridMaker(49)">
|
||||
7 x 7
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{ if .query.controls}}
|
||||
<li class="nav-item dropdown">
|
||||
<input type="hidden" id="defaultPlayer" value="mse" />
|
||||
<a id="defaultPlayerMenu" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="nav-link dropdown-toggle">MSE</a>
|
||||
<ul aria-labelledby="defaultPlayerMenu" class="dropdown-menu custom-dropdown">
|
||||
<li><a href="#" class="dropdown-item" onclick="defaultPlayer('mse',this)">MSE</a></li>
|
||||
<li><a href="#" class="dropdown-item" onclick="defaultPlayer('hls',this)">HLS</a></li>
|
||||
<li><a href="#" class="dropdown-item" onclick="defaultPlayer('webrtc',this)">WebRTC</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/" role="button"><i class="fas fa-times"></i> Close</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- /.navbar -->
|
||||
<div class="content-wrapper p-0">
|
||||
<div class="content p-0">
|
||||
<div class="container-fluid p-0" style="overflow: hidden;">
|
||||
<div class="grid-wrapper" id="grid-wrapper">
|
||||
|
||||
</div>
|
||||
<div class="main-player-wrapper d-none">
|
||||
|
||||
<div class="main-player" data-player="none" data-uuid="0">
|
||||
<video class="main-video-player" id="videoPlayer" autoplay muted playsinline></video>
|
||||
<div class="play-info"> </div>
|
||||
<img class="loader d-none" src="/../static/img/loader.svg" />
|
||||
<canvas id="canvas" class="d-none"></canvas>
|
||||
<input type="hidden" id="uuid" value="0" />
|
||||
</div>
|
||||
<a onclick="closeMain()"><i class="fas fa-times"></i></a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- foot -->
|
||||
</div>
|
||||
<!-- content-wrapper -->
|
||||
<!-- STREAMS LIST -->
|
||||
<aside class="control-sidebar custom">
|
||||
<!-- Control sidebar content goes here -->
|
||||
<p>
|
||||
<a class="control-sidebar-close-btn pull-right" href="#" onclick="$('.control-sidebar').ControlSidebar('collapse')"><i class="fas fa-times"></i></a>
|
||||
</p>
|
||||
|
||||
<h5>Available streams:</h5>
|
||||
<input type="hidden" id="player-index" value="0" />
|
||||
<div class="row">
|
||||
{{ range $key, $value := .streams }}
|
||||
<div class="col-12" id="{{ $key }}">
|
||||
<div class="card card-success">
|
||||
<div id="carousel_{{$key}}" class="carousel slide" data-ride="carousel">
|
||||
|
||||
<div class="carousel-inner">
|
||||
{{if eq (len $value.Channels) 1}}
|
||||
<div class="carousel-item active">
|
||||
<a onclick="play('{{ $key }}',null,0)" href="#"><img class="d-block w-100 stream-img" channel="0" src="/../static/img/noimage.svg"></a>
|
||||
<div class="carousel-caption d-none d-md-block">
|
||||
<h5>{{$value.Name}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{ range $k, $v := .Channels }}
|
||||
{{ if eq $k "1"}}
|
||||
<div class="carousel-item active">
|
||||
<a onclick="play('{{ $key }}',null,{{$k}})" href="#"><img class="d-block w-100 stream-img" channel="{{$k}}" src="/../static/img/noimage.svg"></a>
|
||||
<div class="carousel-caption d-none d-md-block">
|
||||
<h5>{{$value.Name}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
<!-- ./wrapper -->
|
||||
<script>
|
||||
let streams = {{ .streams }};
|
||||
let presetOptions = {{ .options }};
|
||||
let get_query={{.query}};
|
||||
</script>
|
||||
<script src="/../static/plugins/jquery/jquery.min.js"></script>
|
||||
<script src="/../static/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/../static/js/adminlte.min.js"></script>
|
||||
<script src="/../static/plugins/sweetalert2/sweetalert2.min.js"></script>
|
||||
<script src="/../static/js/index.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<!-- end foot -->
|
||||
<script src="/../static/plugins/hlsjs/hls.min.js"></script>
|
||||
<script>
|
||||
let colordebug = false;
|
||||
|
||||
let players = {};
|
||||
$(document).ready(() => {
|
||||
logger(9, 'page is ready')
|
||||
changeBackground(localStorage.getItem('backgroundImage'));
|
||||
if('grid' in get_query){
|
||||
let get_grid=get_query.grid[0]||4;
|
||||
multiviewGrid('set', get_grid);
|
||||
}
|
||||
if (presetOptions.grid != 0) {
|
||||
multiviewGrid('set', presetOptions.grid);
|
||||
}
|
||||
|
||||
if (presetOptions.player != null) {
|
||||
localStorage.setItem('multiviewPlayers', JSON.stringify(presetOptions.player));
|
||||
}
|
||||
gridMaker(multiviewGrid('get'));
|
||||
restoreStreams();
|
||||
});
|
||||
|
||||
function defaultPlayer(type, el) {
|
||||
$('#defaultPlayer').val(type);
|
||||
$(el).closest('.nav-item').children('a').html($(el).text());
|
||||
}
|
||||
|
||||
function gridMaker(col = 4) {
|
||||
let grid_sizes=[1,4,6,9,12,16,25,36,49];
|
||||
col = parseInt(col);
|
||||
col=grid_sizes.filter(v => v >= col)[0]||4;
|
||||
let colW;
|
||||
switch (col) {
|
||||
case 1:
|
||||
colW = 'grid-1';
|
||||
break;
|
||||
case 6:
|
||||
colW = 'grid-6';
|
||||
break;
|
||||
case 9:
|
||||
colW = 'grid-9';
|
||||
break;
|
||||
case 12:
|
||||
colW = 'grid-12';
|
||||
break;
|
||||
case 16:
|
||||
colW = 'grid-16';
|
||||
break;
|
||||
|
||||
case 25:
|
||||
colW = 'grid-25';
|
||||
break;
|
||||
case 36:
|
||||
colW = 'grid-36';
|
||||
break;
|
||||
case 49:
|
||||
colW = 'grid-49';
|
||||
break;
|
||||
default:
|
||||
colW = '';
|
||||
break;
|
||||
}
|
||||
let memory = localStorage.getItem('multiviewPlayers');
|
||||
destroyGrid();
|
||||
for (var i = 0; i < col; i++) {
|
||||
$('#grid-wrapper').append(
|
||||
`<div class=" player ` + colW + ` empty" data-player="none" data-uuid="0">
|
||||
<div class="play-info"></div>
|
||||
<video class="video-class empty" autoplay muted playsinline></video>
|
||||
` +
|
||||
`<a class="remove-btn" onclick="destoyPlayer(` + i + `)"><i class="fas fa-times"></i></a>` +
|
||||
`<a class="add-stream-btn" onclick="openChoise(this)"><i class="fas fa-plus"></i><br />Click to select stream</a>` +
|
||||
`<a href="#" class="btn btn-info btn-xs btn-play-main d-none" onclick="playMainStream(this)"><i class="fas fa-expand"></i> Expand</a>` +
|
||||
`<img class="loader d-none" src="/../static/img/loader.svg" />` +
|
||||
`</div>`);
|
||||
}
|
||||
multiviewGrid('set', col);
|
||||
addEventListenerToVideo();
|
||||
$('.grid-maker').removeClass('active');
|
||||
$('.grid-maker[grid="' + col + '"]').addClass('active');
|
||||
if (memory != null) {
|
||||
localStorage.setItem('multiviewPlayers', memory);
|
||||
restoreStreams();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function addEventListenerToVideo() {
|
||||
$('.video-class').each(function() {
|
||||
let _this = this;
|
||||
let index = $(this).closest('.player').index();
|
||||
let uuid = $(this).closest('.player').attr('data-uuid');
|
||||
|
||||
this.addEventListener('loadeddata', () => {
|
||||
_this.play();
|
||||
makePic(this, $(_this).closest('.player').attr('data-uuid'), 1);
|
||||
logger(index, '[video]: loadeddata');
|
||||
});
|
||||
this.addEventListener('stalled', () => {
|
||||
logger(index, '[video]: stalled');
|
||||
});
|
||||
this.addEventListener('pause', () => {
|
||||
if (_this.currentTime > _this.buffered.end(_this.buffered.length - 1)) {
|
||||
_this.currentTime = _this.buffered.end(_this.buffered.length - 1) - 0.1;
|
||||
_this.play();
|
||||
}
|
||||
logger(index, '[video]: pause');
|
||||
});
|
||||
|
||||
this.addEventListener('error', (e) => {
|
||||
logger(index, '[video]: error', e);
|
||||
//console.log(e);
|
||||
});
|
||||
|
||||
this.addEventListener('abort', () => {
|
||||
logger(index, '[video]: abort');
|
||||
});
|
||||
|
||||
this.addEventListener('emptied', (e) => {
|
||||
logger(index, '[video]: emptied');
|
||||
});
|
||||
|
||||
this.addEventListener('ended', (e) => {
|
||||
logger(index, '[video]: ended');
|
||||
});
|
||||
|
||||
this.addEventListener('play', (e) => {
|
||||
logger(index, '[video]: play');
|
||||
});
|
||||
|
||||
this.addEventListener('suspend', (e) => {
|
||||
logger(index, '[video]: suspend');
|
||||
});
|
||||
this.addEventListener('waiting', (e) => {
|
||||
logger(index, '[video]: waiting');
|
||||
});
|
||||
this.addEventListener('canplaythrough', (e) => {
|
||||
$(_this).closest('div').find('.loader').addClass('d-none');
|
||||
logger(index, '[video]: canplaythrough');
|
||||
});
|
||||
this.addEventListener('playing', (e) => {
|
||||
$(_this).closest('div').find('.loader').addClass('d-none');
|
||||
logger(index, '[video]: playing');
|
||||
});
|
||||
this.addEventListener('loadedmetadata', (e) => {
|
||||
logger(index, '[video]: loadedmetadata');
|
||||
});
|
||||
this.addEventListener('loadstart', (e) => {
|
||||
logger(index, '[video]: loadstart');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$('.main-video-player')[0].addEventListener('canplaythrough', (e) => {
|
||||
$('.main-player').find('.loader').addClass('d-none');
|
||||
logger(0, '[video]: canplaythrough');
|
||||
});
|
||||
$('.main-video-player')[0].addEventListener('playing', (e) => {
|
||||
$('.main-player').find('.loader').addClass('d-none');
|
||||
logger(0, '[video]: canplaythrough');
|
||||
});
|
||||
|
||||
$("#videoPlayer")[0].addEventListener('loadeddata', () => {
|
||||
$("#videoPlayer")[0].play();
|
||||
let browser = browserDetector();
|
||||
if (!browser.safari) {
|
||||
makePic();
|
||||
}
|
||||
});
|
||||
|
||||
function destroyGrid() {
|
||||
$('.player').each(function(index) {
|
||||
destoyPlayer(index);
|
||||
});
|
||||
$('#grid-wrapper').empty();
|
||||
}
|
||||
|
||||
function openChoise(dom) {
|
||||
$('#player-index').val($(dom).closest('.player').index());
|
||||
$('.control-sidebar').ControlSidebar('show')
|
||||
|
||||
}
|
||||
|
||||
function play(uuid, index, chan, typePlayer) {
|
||||
$('#uuid').val(uuid);
|
||||
if (typeof(index) == 'undefined' || index == null) {
|
||||
index = $('#player-index').val();
|
||||
}
|
||||
let videoPlayerVar = $('.main-player');
|
||||
if (index != 'main') {
|
||||
videoPlayerVar = $('.player').eq(index);
|
||||
}
|
||||
|
||||
$('.control-sidebar').ControlSidebar('collapse');
|
||||
destoyPlayer(index);
|
||||
videoPlayerVar.removeClass('empty');
|
||||
videoPlayerVar.find('video').removeClass('empty');
|
||||
|
||||
|
||||
let playerType = $('#defaultPlayer').val();
|
||||
if (!!typePlayer) {
|
||||
playerType = typePlayer;
|
||||
}
|
||||
videoPlayerVar.attr('data-player', playerType);
|
||||
videoPlayerVar.attr('data-uuid', uuid);
|
||||
|
||||
let channel = 0;
|
||||
|
||||
if (typeof(streams[uuid].channels[1]) !== "undefined") {
|
||||
channel = 1;
|
||||
}
|
||||
|
||||
if (index == 'main') {
|
||||
channel = 0;
|
||||
} else {
|
||||
packStreamms(index, uuid, chan, playerType);
|
||||
}
|
||||
if (colordebug) {
|
||||
videoPlayerVar.find('.play-info').html('Stream: ' + streams[uuid].name + ' | player type:' + playerType + ' | channel: ' + channel);
|
||||
}
|
||||
videoPlayerVar.find('.loader').removeClass('d-none');
|
||||
//fix stalled video in safari
|
||||
videoPlayerVar.find('video')[0].addEventListener('pause', () => {
|
||||
if (videoPlayerVar.find('video')[0].currentTime > videoPlayerVar.find('video')[0].buffered.end((videoPlayerVar.find('video')[0].buffered.length - 1))) {
|
||||
videoPlayerVar.find('video')[0].currentTime = videoPlayerVar.find('video')[0].buffered.end((videoPlayerVar.find('video')[0].buffered.length - 1)) - 0.1;
|
||||
videoPlayerVar.find('video')[0].play();
|
||||
}
|
||||
});
|
||||
|
||||
switch (playerType) {
|
||||
case 'hls':
|
||||
let url = '/stream/' + uuid + '/channel/' + channel + '/hls/live/index.m3u8';
|
||||
|
||||
if (videoPlayerVar.find('video')[0].canPlayType('application/vnd.apple.mpegurl')) {
|
||||
videoPlayerVar.find('video')[0].src = url;
|
||||
videoPlayerVar.find('video')[0].load();
|
||||
} else if (Hls.isSupported()) {
|
||||
players[index] = new Hls({
|
||||
manifestLoadingTimeOut: 60000
|
||||
});
|
||||
players[index].loadSource(url);
|
||||
players[index].attachMedia(videoPlayerVar.find('video')[0]);
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Oops...',
|
||||
text: 'Your browser don`t support hls '
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'webrtc':
|
||||
players[index] = new WebRTCPlayer(uuid, videoPlayerVar, channel);
|
||||
players[index].playWebrtc();
|
||||
break;
|
||||
case 'mse':
|
||||
default:
|
||||
|
||||
players[index] = new msePlayer(uuid, videoPlayerVar, channel);
|
||||
players[index].playMse();
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function destoyPlayer(index) {
|
||||
let videoPlayerVar = $('.main-player');
|
||||
if (index != 'main') {
|
||||
videoPlayerVar = $('.player').eq(index);
|
||||
}
|
||||
let type = videoPlayerVar.attr('data-player');
|
||||
videoPlayerVar.addClass('empty');
|
||||
videoPlayerVar.find('video').addClass('empty')
|
||||
|
||||
switch (type) {
|
||||
case 'hls':
|
||||
if (!!players[index]) {
|
||||
players[index].destroy();
|
||||
delete players[index];
|
||||
}
|
||||
break;
|
||||
case 'mse':
|
||||
players[index].destroy();
|
||||
delete players[index];
|
||||
|
||||
break;
|
||||
case 'webrtc':
|
||||
players[index].destroy();
|
||||
delete players[index];
|
||||
break;
|
||||
default:
|
||||
|
||||
break;
|
||||
}
|
||||
videoPlayerVar.attr('data-player', 'none');
|
||||
videoPlayerVar.attr('data-uuid', 0);
|
||||
videoPlayerVar.find('.play-info').html('');
|
||||
videoPlayerVar.find('video')[0].src = '';
|
||||
videoPlayerVar.find('video')[0].load();
|
||||
videoPlayerVar.find('.loader').addClass('d-none');
|
||||
|
||||
unpackStreams(index);
|
||||
}
|
||||
|
||||
function expand(element) {
|
||||
fullscreenOn($('#grid-wrapper').parent()[0]);
|
||||
}
|
||||
|
||||
function playMainStream(element) {
|
||||
let uuid = $(element).closest('.player').attr('data-uuid');
|
||||
if (uuid == 0) {
|
||||
return;
|
||||
}
|
||||
$('.main-player-wrapper').removeClass('d-none');
|
||||
play(uuid, 'main');
|
||||
}
|
||||
|
||||
function closeMain() {
|
||||
destoyPlayer('main');
|
||||
$('.main-player-wrapper').addClass('d-none');
|
||||
}
|
||||
/*************************mse obect **************************/
|
||||
function msePlayer(uuid, videoPlayerVar, channel) {
|
||||
this.ws = null,
|
||||
this.video = videoPlayerVar.find('video')[0],
|
||||
this.sound=false,
|
||||
this.mseSourceBuffer = null,
|
||||
this.mse = null,
|
||||
this.mseQueue = [],
|
||||
this.mseStreamingStarted = false,
|
||||
this.uuid = uuid,
|
||||
this.channel = channel || 0;
|
||||
this.timeout = null;
|
||||
this.checktime = null;
|
||||
this.checktimecounter = 0;
|
||||
this.playMse = function() {
|
||||
let _this = this;
|
||||
logger(videoPlayerVar.index(),
|
||||
'func playMse',
|
||||
'streams: ' + uuid,
|
||||
'channel: ' + channel);
|
||||
|
||||
this.mse = new MediaSource();
|
||||
this.video.src = window.URL.createObjectURL(this.mse);
|
||||
|
||||
let potocol = 'ws';
|
||||
if (location.protocol == 'https:') {
|
||||
potocol = 'wss';
|
||||
}
|
||||
|
||||
let ws_url = potocol + '://' + location.host + '/stream/' + this.uuid + '/channel/' + this.channel + '/mse?uuid=' + this.uuid + '&channel=' + this.channel;
|
||||
|
||||
this.mse.addEventListener('sourceopen', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: sourceopen');
|
||||
_this.ws = new WebSocket(ws_url);
|
||||
_this.ws.binaryType = "arraybuffer";
|
||||
_this.ws.onopen = function(event) {
|
||||
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[websocket]: connected');
|
||||
}
|
||||
|
||||
_this.ws.onclose = function(event) {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[websocket]: closed');
|
||||
if (_this.timeout != null) {
|
||||
clearInterval(_this.timeout);
|
||||
_this.timeout = null;
|
||||
}
|
||||
_this.timeout = setTimeout(() => {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[websocket]: timeouted func play');
|
||||
play(uuid, videoPlayerVar.index(), channel, 'mse')
|
||||
}, 15000)
|
||||
|
||||
|
||||
}
|
||||
_this.ws.onerror = (e) => {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[websocket]: error');
|
||||
}
|
||||
_this.ws.onmessage = function(event) {
|
||||
_this.checkStalled();
|
||||
let data = new Uint8Array(event.data);
|
||||
if (data[3] == 24) {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[data]: init_file');
|
||||
}
|
||||
|
||||
if (data[0] == 9) {
|
||||
decoded_arr = data.slice(1);
|
||||
if (window.TextDecoder) {
|
||||
mimeCodec = new TextDecoder("utf-8").decode(decoded_arr);
|
||||
} else {
|
||||
mimeCodec = Utf8ArrayToStr(decoded_arr);
|
||||
}
|
||||
if(mimeCodec.indexOf(',')>0){
|
||||
_this.sound=true;
|
||||
}
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[codec]: ' + mimeCodec);
|
||||
//console.log(mimeCodec);
|
||||
_this.mseSourceBuffer = _this.mse.addSourceBuffer('video/mp4; codecs="' + mimeCodec + '"');
|
||||
_this.mseSourceBuffer.mode = "segments"
|
||||
_this.mseSourceBuffer.addEventListener("updateend", _this.pushPacket.bind(_this));
|
||||
|
||||
} else {
|
||||
_this.readPacket(event.data);
|
||||
}
|
||||
};
|
||||
}, false);
|
||||
|
||||
this.mse.addEventListener('sourceended', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: sourceended');
|
||||
})
|
||||
this.mse.addEventListener('sourceclose', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: sourceclose');
|
||||
})
|
||||
|
||||
this.mse.addEventListener('error', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: error');
|
||||
})
|
||||
this.mse.addEventListener('abort', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: abort');
|
||||
})
|
||||
this.mse.addEventListener('updatestart', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: updatestart');
|
||||
})
|
||||
this.mse.addEventListener('update', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: update');
|
||||
})
|
||||
this.mse.addEventListener('updateend', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: updateend');
|
||||
})
|
||||
this.mse.addEventListener('addsourcebuffer', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: addsourcebuffer');
|
||||
})
|
||||
this.mse.addEventListener('removesourcebuffer', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: removesourcebuffer');
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
this.readPacket = function(packet) {
|
||||
if (!this.mseStreamingStarted) {
|
||||
try {
|
||||
this.mseSourceBuffer.appendBuffer(packet);
|
||||
this.mseStreamingStarted = true;
|
||||
} catch (e) {
|
||||
logger(videoPlayerVar.index(),
|
||||
'readPacket error',
|
||||
'streams: ' + uuid,
|
||||
'channel: ' + channel);
|
||||
console.log(e);
|
||||
|
||||
play(uuid, videoPlayerVar.index(), channel, 'mse');
|
||||
|
||||
} finally {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
this.mseQueue.push(packet);
|
||||
|
||||
if (!this.mseSourceBuffer.updating) {
|
||||
this.pushPacket();
|
||||
}
|
||||
},
|
||||
|
||||
this.pushPacket = function() {
|
||||
let _this = this;
|
||||
if (!_this.mseSourceBuffer.updating) {
|
||||
if (_this.mseQueue.length > 0) {
|
||||
packet = _this.mseQueue.shift();
|
||||
|
||||
try {
|
||||
_this.mseSourceBuffer.appendBuffer(packet)
|
||||
} catch (e) {
|
||||
logger(videoPlayerVar.index(),
|
||||
'pushPacket error',
|
||||
'streams: ' + uuid,
|
||||
'channel: ' + channel);
|
||||
console.log(e);
|
||||
|
||||
play(uuid, videoPlayerVar.index(), channel, 'mse');
|
||||
} finally {
|
||||
|
||||
}
|
||||
} else {
|
||||
_this.mseStreamingStarted = false;
|
||||
}
|
||||
}
|
||||
if (_this.video.buffered.length > 0) {
|
||||
if (typeof document.hidden !== "undefined" && document.hidden) {
|
||||
if(!_this.sound){
|
||||
_this.video.currentTime = _this.video.buffered.end((_this.video.buffered.length - 1)) - 0.5;
|
||||
}
|
||||
} else {
|
||||
if ((_this.video.buffered.end((_this.video.buffered.length - 1)) - _this.video.currentTime) > 60) {
|
||||
_this.video.currentTime = _this.video.buffered.end((_this.video.buffered.length - 1)) - 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.checkStalled = function() {
|
||||
if (!!this.video.currentTime) {
|
||||
if (this.video.currentTime == this.checktime) {
|
||||
this.checktimecounter += 1;
|
||||
} else {
|
||||
this.checktimecounter = 0;
|
||||
}
|
||||
}
|
||||
if (this.checktimecounter > 10) {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[FIX]: player not move');
|
||||
play(uuid, videoPlayerVar.index(), channel, 'mse');
|
||||
}
|
||||
this.checktime = this.video.currentTime;
|
||||
|
||||
},
|
||||
this.destroy = function() {
|
||||
if (this.timeout != null) {
|
||||
clearInterval(this.timeout);
|
||||
}
|
||||
if (this.ws != null) {
|
||||
|
||||
this.ws.onclose = null;
|
||||
this.ws.close(1000, "stop streaming");
|
||||
}
|
||||
|
||||
|
||||
|
||||
logger(videoPlayerVar.index(),
|
||||
'Event: PlayerDestroy',
|
||||
'streams: ' + uuid,
|
||||
'channel: ' + channel);
|
||||
}
|
||||
}
|
||||
/*************************end mse obect **************************/
|
||||
/*************************WEBRTC obect **************************/
|
||||
function WebRTCPlayer(uuid, videoPlayerVar, channel) {
|
||||
this.webrtc = null;
|
||||
this.webrtcSendChannel = null;
|
||||
this.webrtcSendChannelInterval = null;
|
||||
this.uuid = uuid;
|
||||
this.video = videoPlayerVar.find('video')[0];
|
||||
this.channel = channel || 0;
|
||||
this.playWebrtc = function() {
|
||||
var _this = this;
|
||||
this.webrtc = new RTCPeerConnection({
|
||||
iceServers: [{
|
||||
urls: ["stun:stun.l.google.com:19302"]
|
||||
}]
|
||||
});
|
||||
this.webrtc.onnegotiationneeded = this.handleNegotiationNeeded.bind(this);
|
||||
this.webrtc.ontrack = function(event) {
|
||||
console.log(event.streams.length + ' track is delivered');
|
||||
_this.video.srcObject = event.streams[0];
|
||||
_this.video.play();
|
||||
}
|
||||
this.webrtc.addTransceiver('video', {
|
||||
'direction': 'sendrecv'
|
||||
});
|
||||
this.webrtcSendChannel = this.webrtc.createDataChannel('foo');
|
||||
this.webrtcSendChannel.onclose = (e) => console.log('sendChannel has closed', e);
|
||||
this.webrtcSendChannel.onopen = () => {
|
||||
console.log('sendChannel has opened');
|
||||
this.webrtcSendChannel.send('ping');
|
||||
this.webrtcSendChannelInterval = setInterval(() => {
|
||||
this.webrtcSendChannel.send('ping');
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
this.webrtcSendChannel.onmessage = e => console.log(e.data);
|
||||
},
|
||||
this.handleNegotiationNeeded = async function() {
|
||||
var _this = this;
|
||||
|
||||
offer = await _this.webrtc.createOffer();
|
||||
await _this.webrtc.setLocalDescription(offer);
|
||||
$.post("/stream/" + _this.uuid + "/channel/" + this.channel + "/webrtc?uuid=" + _this.uuid + "&channel=" + this.channel, {
|
||||
data: btoa(_this.webrtc.localDescription.sdp)
|
||||
}, function(data) {
|
||||
try {
|
||||
_this.webrtc.setRemoteDescription(new RTCSessionDescription({
|
||||
type: 'answer',
|
||||
sdp: atob(data)
|
||||
}))
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
this.destroy = function() {
|
||||
clearInterval(this.webrtcSendChannelInterval);
|
||||
this.webrtc.close();
|
||||
this.video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
/*********************FULSCREEN******************/
|
||||
function fullscreenEnabled() {
|
||||
return !!(
|
||||
document.fullscreenEnabled ||
|
||||
document.webkitFullscreenEnabled ||
|
||||
document.mozFullScreenEnabled ||
|
||||
document.msFullscreenEnabled
|
||||
);
|
||||
}
|
||||
|
||||
function fullscreenOn(elem) {
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen();
|
||||
} else if (elem.mozRequestFullScreen) {
|
||||
elem.mozRequestFullScreen();
|
||||
} else if (elem.webkitRequestFullscreen) {
|
||||
elem.webkitRequestFullscreen();
|
||||
} else if (elem.msRequestFullscreen) {
|
||||
elem.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
function fullscreenOff() {
|
||||
if (document.requestFullscreen) {
|
||||
document.requestFullscreen();
|
||||
} else if (document.webkitRequestFullscreen) {
|
||||
document.webkitRequestFullscreen();
|
||||
} else if (document.mozRequestFullscreen) {
|
||||
document.mozRequestFullScreen();
|
||||
}
|
||||
}
|
||||
|
||||
function packStreamms(index, uuid, channel, type) {
|
||||
let multiviewPlayers;
|
||||
if (localStorage.getItem('multiviewPlayers') != null) {
|
||||
multiviewPlayers = JSON.parse(localStorage.getItem('multiviewPlayers'));
|
||||
} else {
|
||||
multiviewPlayers = {};
|
||||
}
|
||||
multiviewPlayers[index] = {
|
||||
uuid: uuid,
|
||||
channel: channel,
|
||||
playerType: type
|
||||
}
|
||||
localStorage.setItem('multiviewPlayers', JSON.stringify(multiviewPlayers));
|
||||
}
|
||||
|
||||
function unpackStreams(index) {
|
||||
if (localStorage.getItem('multiviewPlayers') != null) {
|
||||
let multiviewPlayers = JSON.parse(localStorage.getItem('multiviewPlayers'));
|
||||
delete multiviewPlayers[index];
|
||||
localStorage.setItem('multiviewPlayers', JSON.stringify(multiviewPlayers));
|
||||
}
|
||||
}
|
||||
|
||||
function restoreStreams() {
|
||||
if (localStorage.getItem('multiviewPlayers') != null) {
|
||||
|
||||
let multiviewPlayers = JSON.parse(localStorage.getItem('multiviewPlayers'));
|
||||
if (Object.keys(multiviewPlayers).length > 0) {
|
||||
$.each(multiviewPlayers, function(key, val) {
|
||||
|
||||
if (val.uuid in streams && val.channel in streams[val.uuid].channels) {
|
||||
if ($('.player').eq(key).length > 0) {
|
||||
play(val.uuid, key, val.channel, val.playerType);
|
||||
}
|
||||
|
||||
} else {
|
||||
unpackStreams(key);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (Object.keys(streams).length > 0) {
|
||||
gridMaker(Object.keys(streams).length);
|
||||
let playerIndex = 0;
|
||||
let gridSize = $('.player').length;
|
||||
$.each(streams, function(uuid, params) {
|
||||
//console.log(i,uuid);
|
||||
if (playerIndex < gridSize) {
|
||||
let channel = 0;
|
||||
if ('1' in params.channels) {
|
||||
channel = 1;
|
||||
}
|
||||
play(uuid, playerIndex, channel, 'mse');
|
||||
playerIndex++;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function multiviewGrid(type, grid) {
|
||||
let defGrid = 25;
|
||||
switch (type) {
|
||||
case 'set':
|
||||
localStorage.setItem('multiviewGrid', grid);
|
||||
break;
|
||||
case 'get':
|
||||
if (localStorage.getItem('multiviewGrid') != null) {
|
||||
return localStorage.getItem('multiviewGrid');
|
||||
} else {
|
||||
return defGrid
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return defGrid
|
||||
}
|
||||
return defGrid
|
||||
}
|
||||
|
||||
$('#grid-wrapper').on('dblclick', '.player', function() {
|
||||
$(this).find('.btn-play-main').click();
|
||||
});
|
||||
$('.main-player').on('dblclick', function() {
|
||||
closeMain()
|
||||
});
|
||||
|
||||
function changeBackground(num) {
|
||||
let back = '/../static/img/';
|
||||
localStorage.setItem('backgroundImage', num);
|
||||
switch (num) {
|
||||
case 'green':
|
||||
back += 'green.jpg'
|
||||
break;
|
||||
case 'red':
|
||||
back += 'red.jpg'
|
||||
break;
|
||||
case 'white':
|
||||
back += 'white.jpg'
|
||||
break;
|
||||
default:
|
||||
back += 'back.jpg'
|
||||
}
|
||||
$('.img-background').css('background-image', 'url("' + back + '")');
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
|
||||
<title>RTSPtoWEB</title>
|
||||
<link rel="stylesheet" href="/../static/plugins/fontawesome-free/css/all.min.css">
|
||||
<link rel="stylesheet" href="/../static/css/adminlte.min.css">
|
||||
<link rel="stylesheet" href="/../static/plugins/sweetalert2/sweetalert2.min.css">
|
||||
<link rel="stylesheet" href="/../static/css/index.css">
|
||||
<link href="/../static/css/google-fonts.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="hold-transition sidebar-mini">
|
||||
<div class="wrapper">
|
||||
|
||||
<!-- Navbar -->
|
||||
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
|
||||
<!-- Left navbar links -->
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a>
|
||||
</li>
|
||||
<li class="nav-item d-none d-sm-inline-block">
|
||||
<a href="/" class="nav-link">Home</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/pages/stream/add" role="button"><i class="fas fa-plus-square"></i> Add stream</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<aside class="main-sidebar sidebar-dark-primary elevation-4">
|
||||
<a href="/" class="brand-link">
|
||||
<img src="/static/img/pic.svg" alt="AdminLTE Logo" class="brand-image img-circle" style="opacity: .8">
|
||||
<span class="brand-text font-weight-light">RTSPtoWEB</span>
|
||||
</a>
|
||||
|
||||
<div class="sidebar">
|
||||
<nav class="mt-2">
|
||||
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link
|
||||
{{ if eq .page "index"}}
|
||||
active
|
||||
{{end}}
|
||||
">
|
||||
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||
<p>
|
||||
Dashboard
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-header">NAVIGATION</li>
|
||||
<li class="nav-item ">
|
||||
<a href="/pages/stream/list" class="nav-link {{ if (eq .page "stream_list") }}
|
||||
active
|
||||
{{end}}">
|
||||
<i class="fas fa-list-alt nav-icon"></i>
|
||||
<p>Streams list</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pages/stream/add" class="nav-link {{ if (eq .page "add_stream") }}
|
||||
active
|
||||
{{end}}">
|
||||
<i class="fas fa-plus-square nav-icon"></i>
|
||||
<p>Add stream</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item ">
|
||||
<a href="/pages/multiview/full?controls" class="nav-link {{ if (eq .page "fullscreenmulti") }}
|
||||
active
|
||||
{{end}}">
|
||||
<i class="fas fa-th-large nav-icon"></i>
|
||||
<p>Full multiview</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-header">MISCELLANEOUS</li>
|
||||
<li class="nav-item">
|
||||
<a href="/pages/documentation" class="nav-link
|
||||
{{ if eq .page "documentation"}}
|
||||
active
|
||||
{{end}}">
|
||||
<i class="nav-icon fas fa-file"></i>
|
||||
<p>Documentation</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<!-- /.sidebar-menu -->
|
||||
</div>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
|
||||
<!-- Content Wrapper. Contains page content -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
|
||||
<!-- /.content-header -->
|
||||
@@ -0,0 +1,129 @@
|
||||
{{template "head.tmpl" .}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">Dashboard</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Dashboard</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
</div>
|
||||
<!-- Main content -->
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h5 class="mt-4 mb-2">Streams ({{ len .streams}})</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3 ">
|
||||
{{ range $key, $value := .streams }}
|
||||
<div class="col-12 col-sm-6 col-md-3" id="{{ $key }}">
|
||||
|
||||
<div class="card card-outline card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title one-line-header">{{.Name}}</h3>
|
||||
<div class="card-tools">
|
||||
<span data-toggle="tooltip" title="avaliable channels" class="badge badge-success">{{len .Channels }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
|
||||
<div id="carousel_{{$key}}" class="carousel slide" data-ride="carousel">
|
||||
<ol class="carousel-indicators">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<li data-target="#carousel_{{$key}}" data-slide-to="{{$k}}" class="{{ if eq $k "0"}} active {{end}}"></li>
|
||||
{{end}}
|
||||
</ol>
|
||||
<div class="carousel-inner">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<div class="carousel-item {{ if eq $k "0"}} active {{end}}">
|
||||
<img class="d-block w-100 stream-img fix-height" channel="{{$k}}" src="/../static/img/noimage.svg">
|
||||
<div class="carousel-caption d-none d-md-block">
|
||||
<h5>Channel: {{$k}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carousel_{{$key}}" role="button" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carousel_{{$key}}" role="button" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="btn-group stream">
|
||||
{{ if gt (len .Channels) 1}}
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> MSE</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/mse/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> HLS</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/hls/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> WebRTC</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/webrtc/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> ALL</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/all/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/mse/{{$key}}/0"><i class="fas fa-play"></i> MSE</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/hls/{{$key}}/0"><i class="fas fa-play"></i> HLS</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/webrtc/{{$key}}/0"><i class="fas fa-play"></i> WebRTC</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/all/{{$key}}/0"><i class="fas fa-play"></i> ALL</a>
|
||||
{{end}}
|
||||
|
||||
|
||||
|
||||
<a class="btn btn-secondary btn-flat btn-xs" href="/pages/stream/edit/{{$key}}"><i class="fas fa-edit"></i> Edit</a>
|
||||
<a class="btn btn-danger btn-flat btn-xs" onclick="deleteStream('{{ $key }}')" href="#"><i class="fas fa-times"></i> Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
|
||||
</div>
|
||||
<!-- /.content -->
|
||||
|
||||
|
||||
{{template "foot.tmpl" .}}
|
||||
@@ -0,0 +1,596 @@
|
||||
{{template "head.tmpl" .}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">Multiview</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Multiview</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">Grid</h5>
|
||||
|
||||
<div class="card-tools">
|
||||
<button type="button" onclick="expand()" class="btn btn-tool">
|
||||
<i class="fas fa-expand"></i>
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-tool dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fas fa-th-large"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<a href="#" class="dropdown-item" onclick="gridMaker(4)"><i class="fas fa-th-large"></i> four players</a>
|
||||
<a href="#" class="dropdown-item" onclick="gridMaker(6)"><i class="fas fa-grip-horizontal"></i> six players</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<input type="hidden" id="defaultPlayer" value="mse" />
|
||||
<button type="button" class="btn btn-tool dropdown-toggle" data-toggle="dropdown">
|
||||
MSE
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<a href="#" class="dropdown-item" onclick="defaultPlayer('mse',this)">MSE</a>
|
||||
<a href="#" class="dropdown-item" onclick="defaultPlayer('hls',this)">HLS</a>
|
||||
<a href="#" class="dropdown-item" onclick="defaultPlayer('webrtc',this)">WebRTC</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.card-header -->
|
||||
<div class="card-body p-0">
|
||||
<div class="grid-wrapper" id="grid-wrapper">
|
||||
|
||||
</div>
|
||||
<div class="main-player-wrapper d-none">
|
||||
|
||||
<div class="main-player" data-player="none" data-uuid="0">
|
||||
<video autoplay></video>
|
||||
<div class="play-info"> </div>
|
||||
</div>
|
||||
<a onclick="closeMain()"><i class="fas fa-times"></i></a>
|
||||
</div>
|
||||
<!-- STREAMS LIST -->
|
||||
<div class="modal fade" id="choiseChannel" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">Click on stream to play</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="player-index" value="0" />
|
||||
<div class="row">
|
||||
{{ range $key, $value := .streams }}
|
||||
<div class="col-12 col-sm-6" id="{{ $key }}">
|
||||
<div class="card card-success">
|
||||
<div id="carousel_{{$key}}" class="carousel slide" data-ride="carousel">
|
||||
<ol class="carousel-indicators">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<li data-target="#carousel_{{$key}}" data-slide-to="{{$k}}" class="{{ if eq $k "0"}} active {{end}}"></li>
|
||||
{{end}}
|
||||
</ol>
|
||||
<div class="carousel-inner">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<div class="carousel-item {{ if eq $k "0"}} active {{end}}">
|
||||
<a onclick="play('{{ $key }}',null,{{$k}})" href="#"><img class="d-block w-100 stream-img" channel="{{$k}}" src="/../static/img/noimage.svg"></a>
|
||||
<div class="carousel-caption d-none d-md-block">
|
||||
<h5>{{$value.Name}}</h5>
|
||||
<p>Channel: {{$k}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carousel_{{$key}}" role="button" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carousel_{{$key}}" role="button" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ./STREAMS LIST -->
|
||||
</div>
|
||||
<!-- ./card-body -->
|
||||
</div>
|
||||
<!-- /.card -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "foot.tmpl" .}}
|
||||
|
||||
<script src="/../static/plugins/hlsjs/hls.min.js"></script>
|
||||
<script>
|
||||
const playerType = 'webrtc';
|
||||
let players = {};
|
||||
$(document).ready(() => {
|
||||
gridMaker(multiviewGrid('get'));
|
||||
restoreStreams();
|
||||
});
|
||||
|
||||
function defaultPlayer(type, el) {
|
||||
$('#defaultPlayer').val(type);
|
||||
$(el).closest('.btn-group').find('button').html($(el).text());
|
||||
}
|
||||
|
||||
function gridMaker(col = 4) {
|
||||
col = parseInt(col);
|
||||
let colW;
|
||||
switch (col) {
|
||||
case 6:
|
||||
colW = 'six';
|
||||
break;
|
||||
case 9:
|
||||
colW = 'nine';
|
||||
break;
|
||||
case 12:
|
||||
colW = 'twelve';
|
||||
break;
|
||||
case 16:
|
||||
colW = 'sixteen';
|
||||
break;
|
||||
default:
|
||||
colW = '';
|
||||
break;
|
||||
}
|
||||
destroyGrid();
|
||||
for (var i = 0; i < col; i++) {
|
||||
$('#grid-wrapper').append(
|
||||
`<div class=" player ` + colW + `" data-player="none" data-uuid="0">
|
||||
<div class="play-info"></div>
|
||||
<video autoplay muted></video>
|
||||
<div class="control">
|
||||
<a href="#" class="btn btn-success btn-xs" onclick="openChoise(this)"><i class="fas fa-plus"></i> Add</a>
|
||||
<a href="#" class="btn btn-info btn-xs btn-play-main" onclick="playMainStream(this)"><i class="fas fa-expand"></i> Expand</a>
|
||||
<a href="#" class="btn btn-danger btn-xs" onclick="destoyPlayer(` + i + `)"><i class="fas fa-times"></i> Delete</a>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
multiviewGrid('set', col);
|
||||
}
|
||||
|
||||
function destroyGrid() {
|
||||
$('.player').each(function(index) {
|
||||
destoyPlayer(index);
|
||||
});
|
||||
$('#grid-wrapper').empty();
|
||||
}
|
||||
|
||||
function openChoise(dom) {
|
||||
$('#player-index').val($(dom).closest('.player').index());
|
||||
$('#choiseChannel').modal('show');
|
||||
}
|
||||
|
||||
function play(uuid, index, chan, typePlayer) {
|
||||
|
||||
if (typeof(index) == 'undefined' || index == null) {
|
||||
index = $('#player-index').val();
|
||||
}
|
||||
let videoPlayer = $('.main-player');
|
||||
if (index != 'main') {
|
||||
videoPlayer = $('.player').eq(index);
|
||||
}
|
||||
$('#choiseChannel').modal('hide');
|
||||
destoyPlayer(index);
|
||||
videoPlayer.find('video').css('background', '#000');
|
||||
|
||||
let playerType = $('#defaultPlayer').val();
|
||||
if (!!typePlayer) {
|
||||
playerType = typePlayer;
|
||||
}
|
||||
videoPlayer.attr('data-player', playerType);
|
||||
videoPlayer.attr('data-uuid', uuid);
|
||||
|
||||
let channel = 0;
|
||||
|
||||
if (typeof(streams[uuid].channels[1]) !== "undefined") {
|
||||
channel = 1;
|
||||
}
|
||||
if (typeof(chan) !== "undefined") {
|
||||
channel = chan;
|
||||
}
|
||||
if (index == 'main') {
|
||||
channel = 0;
|
||||
} else {
|
||||
packStreamms(index, uuid, chan, playerType);
|
||||
}
|
||||
|
||||
videoPlayer.find('.play-info').html('Stream: ' + streams[uuid].name + ' | player type:' + playerType + ' | channel: ' + channel);
|
||||
//fix stalled video in safari
|
||||
videoPlayer.find('video')[0].addEventListener('pause', () => {
|
||||
if (videoPlayer.find('video')[0].currentTime > videoPlayer.find('video')[0].buffered.end((videoPlayer.find('video')[0].buffered.length - 1))) {
|
||||
videoPlayer.find('video')[0].currentTime = videoPlayer.find('video')[0].buffered.end((videoPlayer.find('video')[0].buffered.length - 1)) - 0.1;
|
||||
videoPlayer.find('video')[0].play();
|
||||
}
|
||||
});
|
||||
|
||||
switch (playerType) {
|
||||
case 'hls':
|
||||
let url = '/stream/' + uuid + '/channel/' + channel + '/hls/live/index.m3u8';
|
||||
|
||||
if (videoPlayer.find('video')[0].canPlayType('application/vnd.apple.mpegurl')) {
|
||||
videoPlayer.find('video')[0].src = url;
|
||||
videoPlayer.find('video')[0].load();
|
||||
} else if (Hls.isSupported()) {
|
||||
players[index] = new Hls({
|
||||
manifestLoadingTimeOut: 60000
|
||||
});
|
||||
players[index].loadSource(url);
|
||||
players[index].attachMedia(videoPlayer.find('video')[0]);
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Oops...',
|
||||
text: 'Your browser don`t support hls '
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'webrtc':
|
||||
players[index] = new WebRTCPlayer(uuid, videoPlayer, channel);
|
||||
players[index].playWebrtc();
|
||||
break;
|
||||
case 'mse':
|
||||
default:
|
||||
|
||||
players[index] = new msePlayer(uuid, videoPlayer, channel);
|
||||
players[index].playMse();
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function destoyPlayer(index) {
|
||||
let videoPlayer = $('.main-player');
|
||||
if (index != 'main') {
|
||||
videoPlayer = $('.player').eq(index);
|
||||
}
|
||||
let type = videoPlayer.attr('data-player');
|
||||
videoPlayer.find('video').css('background', '#343a40');
|
||||
switch (type) {
|
||||
case 'hls':
|
||||
if (!!players[index]) {
|
||||
players[index].destroy();
|
||||
delete players[index];
|
||||
}
|
||||
break;
|
||||
case 'mse':
|
||||
players[index].destroy();
|
||||
delete players[index];
|
||||
|
||||
break;
|
||||
case 'webrtc':
|
||||
players[index].destroy();
|
||||
delete players[index];
|
||||
break;
|
||||
default:
|
||||
|
||||
break;
|
||||
}
|
||||
videoPlayer.attr('data-player', 'none');
|
||||
videoPlayer.attr('data-uuid', 0);
|
||||
videoPlayer.find('.play-info').html('');
|
||||
videoPlayer.find('video')[0].src = '';
|
||||
videoPlayer.find('video')[0].load();
|
||||
|
||||
unpackStreams(index);
|
||||
}
|
||||
|
||||
function expand(element) {
|
||||
fullscreenOn($('#grid-wrapper').parent()[0]);
|
||||
}
|
||||
|
||||
function playMainStream(element) {
|
||||
let uuid = $(element).closest('.player').attr('data-uuid');
|
||||
if (uuid == 0) {
|
||||
return;
|
||||
}
|
||||
$('.main-player-wrapper').removeClass('d-none');
|
||||
play(uuid, 'main');
|
||||
}
|
||||
|
||||
function closeMain() {
|
||||
destoyPlayer('main');
|
||||
$('.main-player-wrapper').addClass('d-none');
|
||||
}
|
||||
/*************************mse obect **************************/
|
||||
function msePlayer(uuid, videoPlayer, channel) {
|
||||
this.ws = null,
|
||||
this.video = videoPlayer.find('video')[0],
|
||||
this.mseSourceBuffer = null,
|
||||
this.mse = null,
|
||||
this.mseQueue = [],
|
||||
this.mseStreamingStarted = false,
|
||||
this.uuid = uuid,
|
||||
this.channel = channel || 0;
|
||||
this.timeout=null;
|
||||
this.playMse = function() {
|
||||
let _this = this;
|
||||
this.mse = new MediaSource();
|
||||
this.video.src = window.URL.createObjectURL(this.mse);
|
||||
|
||||
let potocol = 'ws';
|
||||
if (location.protocol == 'https:') {
|
||||
potocol = 'wss';
|
||||
}
|
||||
|
||||
let ws_url = potocol + '://' + location.host + '/stream/' + this.uuid + '/channel/' + this.channel + '/mse?uuid=' + this.uuid + '&channel=' + this.channel;
|
||||
|
||||
this.mse.addEventListener('sourceopen', function() {
|
||||
_this.ws = new WebSocket(ws_url);
|
||||
_this.ws.binaryType = "arraybuffer";
|
||||
_this.ws.onopen = function(event) {
|
||||
console.log('Connect to ws');
|
||||
}
|
||||
|
||||
_this.ws.onclose = function(event) {
|
||||
console.log('disconnect, try to reconect until 15 sec');
|
||||
|
||||
setTimeout(()=>{
|
||||
|
||||
play(uuid, videoPlayer.index(), channel,'mse')
|
||||
|
||||
},15000)
|
||||
|
||||
|
||||
}
|
||||
_this.ws.onerror=(e)=>{
|
||||
console.log('error, try to reconect until 15 sec');
|
||||
setTimeout(()=>{
|
||||
play(uuid, videoPlayer.index(), channel,'mse')
|
||||
|
||||
},15000)
|
||||
}
|
||||
_this.ws.onmessage = function(event) {
|
||||
let data = new Uint8Array(event.data);
|
||||
if (data[0] == 9) {
|
||||
decoded_arr = data.slice(1);
|
||||
if (window.TextDecoder) {
|
||||
mimeCodec = new TextDecoder("utf-8").decode(decoded_arr);
|
||||
} else {
|
||||
mimeCodec = Utf8ArrayToStr(decoded_arr);
|
||||
}
|
||||
//console.log(mimeCodec);
|
||||
_this.mseSourceBuffer = _this.mse.addSourceBuffer('video/mp4; codecs="' + mimeCodec + '"');
|
||||
_this.mseSourceBuffer.mode = "segments"
|
||||
_this.mseSourceBuffer.addEventListener("updateend", _this.pushPacket.bind(_this));
|
||||
|
||||
} else {
|
||||
_this.readPacket(event.data);
|
||||
}
|
||||
};
|
||||
}, false);
|
||||
|
||||
}
|
||||
|
||||
this.readPacket = function(packet) {
|
||||
if (!this.mseStreamingStarted) {
|
||||
this.mseSourceBuffer.appendBuffer(packet);
|
||||
this.mseStreamingStarted = true;
|
||||
return;
|
||||
}
|
||||
this.mseQueue.push(packet);
|
||||
|
||||
if (!this.mseSourceBuffer.updating) {
|
||||
this.pushPacket();
|
||||
}
|
||||
},
|
||||
|
||||
this.pushPacket = function() {
|
||||
let _this = this;
|
||||
if (!_this.mseSourceBuffer.updating) {
|
||||
if (_this.mseQueue.length > 0) {
|
||||
packet = _this.mseQueue.shift();
|
||||
let view = new Uint8Array(packet);
|
||||
_this.mseSourceBuffer.appendBuffer(packet);
|
||||
} else {
|
||||
_this.mseStreamingStarted = false;
|
||||
}
|
||||
}
|
||||
if (_this.video.buffered.length > 0) {
|
||||
if (typeof document.hidden !== "undefined" && document.hidden) {
|
||||
_this.video.currentTime = _this.video.buffered.end((_this.video.buffered.length - 1)) - 0.5;
|
||||
}else{
|
||||
if( (_this.video.buffered.end((_this.video.buffered.length - 1))-_this.video.currentTime)>60 ){
|
||||
_this.video.currentTime = _this.video.buffered.end((_this.video.buffered.length - 1)) - 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.destroy = function() {
|
||||
this.ws.onclose=null;
|
||||
if(this.timeout!=null){
|
||||
clearInterval(this.timeout);
|
||||
}
|
||||
this.ws.close(1000, "stop streaming");
|
||||
|
||||
}
|
||||
}
|
||||
/*************************end mse obect **************************/
|
||||
/*************************WEBRTC obect **************************/
|
||||
function WebRTCPlayer(uuid, videoPlayer, channel) {
|
||||
this.webrtc = null;
|
||||
this.webrtcSendChannel = null;
|
||||
this.webrtcSendChannelInterval = null;
|
||||
this.uuid = uuid;
|
||||
this.video = videoPlayer.find('video')[0];
|
||||
this.channel = channel || 0;
|
||||
this.playWebrtc = function() {
|
||||
var _this = this;
|
||||
this.webrtc = new RTCPeerConnection({
|
||||
iceServers: [{
|
||||
urls: ["stun:stun.l.google.com:19302"]
|
||||
}]
|
||||
});
|
||||
this.webrtc.onnegotiationneeded = this.handleNegotiationNeeded.bind(this);
|
||||
this.webrtc.ontrack = function(event) {
|
||||
console.log(event.streams.length + ' track is delivered');
|
||||
_this.video.srcObject = event.streams[0];
|
||||
_this.video.play();
|
||||
}
|
||||
this.webrtc.addTransceiver('video', {
|
||||
'direction': 'sendrecv'
|
||||
});
|
||||
this.webrtcSendChannel = this.webrtc.createDataChannel('foo');
|
||||
this.webrtcSendChannel.onclose = (e) => console.log('sendChannel has closed',e);
|
||||
this.webrtcSendChannel.onopen = () => {
|
||||
console.log('sendChannel has opened');
|
||||
this.webrtcSendChannel.send('ping');
|
||||
this.webrtcSendChannelInterval = setInterval(() => {
|
||||
this.webrtcSendChannel.send('ping');
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
this.webrtcSendChannel.onmessage = e => console.log(e.data);
|
||||
},
|
||||
this.handleNegotiationNeeded = async function() {
|
||||
var _this = this;
|
||||
|
||||
offer = await _this.webrtc.createOffer();
|
||||
await _this.webrtc.setLocalDescription(offer);
|
||||
$.post("/stream/" + _this.uuid + "/channel/" + this.channel + "/webrtc?uuid=" + _this.uuid + "&channel=" + this.channel, {
|
||||
data: btoa(_this.webrtc.localDescription.sdp)
|
||||
}, function(data) {
|
||||
try {
|
||||
_this.webrtc.setRemoteDescription(new RTCSessionDescription({
|
||||
type: 'answer',
|
||||
sdp: atob(data)
|
||||
}))
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
this.destroy = function() {
|
||||
clearInterval(this.webrtcSendChannelInterval);
|
||||
this.webrtc.close();
|
||||
this.video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
/*********************FULSCREEN******************/
|
||||
function fullscreenEnabled() {
|
||||
return !!(
|
||||
document.fullscreenEnabled ||
|
||||
document.webkitFullscreenEnabled ||
|
||||
document.mozFullScreenEnabled ||
|
||||
document.msFullscreenEnabled
|
||||
);
|
||||
}
|
||||
|
||||
function fullscreenOn(elem) {
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen();
|
||||
} else if (elem.mozRequestFullScreen) {
|
||||
elem.mozRequestFullScreen();
|
||||
} else if (elem.webkitRequestFullscreen) {
|
||||
elem.webkitRequestFullscreen();
|
||||
} else if (elem.msRequestFullscreen) {
|
||||
elem.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
function fullscreenOff() {
|
||||
if (document.requestFullscreen) {
|
||||
document.requestFullscreen();
|
||||
} else if (document.webkitRequestFullscreen) {
|
||||
document.webkitRequestFullscreen();
|
||||
} else if (document.mozRequestFullscreen) {
|
||||
document.mozRequestFullScreen();
|
||||
}
|
||||
}
|
||||
|
||||
function packStreamms(index, uuid, channel, type) {
|
||||
let multiviewPlayers;
|
||||
if (localStorage.getItem('multiviewPlayers') != null) {
|
||||
multiviewPlayers = JSON.parse(localStorage.getItem('multiviewPlayers'));
|
||||
} else {
|
||||
multiviewPlayers = {};
|
||||
}
|
||||
multiviewPlayers[index] = {
|
||||
uuid: uuid,
|
||||
channel: channel,
|
||||
playerType: type
|
||||
}
|
||||
localStorage.setItem('multiviewPlayers', JSON.stringify(multiviewPlayers));
|
||||
}
|
||||
|
||||
function unpackStreams(index) {
|
||||
if (localStorage.getItem('multiviewPlayers') != null) {
|
||||
let multiviewPlayers = JSON.parse(localStorage.getItem('multiviewPlayers'));
|
||||
delete multiviewPlayers[index];
|
||||
localStorage.setItem('multiviewPlayers', JSON.stringify(multiviewPlayers));
|
||||
}
|
||||
}
|
||||
|
||||
function restoreStreams() {
|
||||
if (localStorage.getItem('multiviewPlayers') != null) {
|
||||
|
||||
let multiviewPlayers = JSON.parse(localStorage.getItem('multiviewPlayers'));
|
||||
if (Object.keys(multiviewPlayers).length > 0) {
|
||||
$.each(multiviewPlayers, function(key, val) {
|
||||
|
||||
if (val.uuid in streams && val.channel in streams[val.uuid].channels) {
|
||||
play(val.uuid, key, val.channel, val.playerType);
|
||||
} else {
|
||||
unpackStreams(key);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function multiviewGrid(type, grid) {
|
||||
//console.log('type, grid')
|
||||
let defGrid = 4;
|
||||
switch (type) {
|
||||
case 'set':
|
||||
localStorage.setItem('multiviewGrid', grid);
|
||||
break;
|
||||
case 'get':
|
||||
if (localStorage.getItem('multiviewGrid') != null) {
|
||||
return localStorage.getItem('multiviewGrid');
|
||||
} else {
|
||||
return defGrid
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return defGrid
|
||||
}
|
||||
return defGrid
|
||||
}
|
||||
|
||||
$('#grid-wrapper').on('dblclick','.player',function (){
|
||||
$(this).find('.btn-play-main').click();
|
||||
});
|
||||
$('.main-player').on('dblclick',function (){
|
||||
closeMain()
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,556 @@
|
||||
{{template "head.tmpl" .}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">Comparsion page</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Comparsion page</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- player -->
|
||||
<div class="col-12 col-md-8 col-lg-9">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title one-line-header">HLS</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<video class="videoPlayer" vtype="hls" id="videoPlayer" autoplay controls muted playsinline></video>
|
||||
<canvas id="canvas" class="d-none"></canvas>
|
||||
</div>
|
||||
<input type="hidden" id="uuid" value="{{ .uuid }}" />
|
||||
<input type="hidden" id="channel" value="{{ .channel }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title one-line-header">HLSLL</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<video class="videoPlayer" vtype="hlsll" autoplay controls muted playsinline></video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title one-line-header">MSE</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<video class="videoPlayer" vtype="mse" autoplay controls muted playsinline></video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title one-line-header">WEBRTC</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<video class="videoPlayer" vtype="webrtc" autoplay controls muted playsinline></video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 col-lg-3">
|
||||
<div class="row">
|
||||
{{ range $key, $value := .streams }}
|
||||
<div class="col-12" id="{{ $key }}">
|
||||
|
||||
<div class="card card-outline card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title one-line-header">{{.Name}}</h3>
|
||||
<div class="card-tools">
|
||||
<span data-toggle="tooltip" title="avaliable channels" class="badge badge-success">{{len .Channels }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
|
||||
<div id="carousel_{{$key}}" class="carousel slide" data-ride="carousel">
|
||||
<ol class="carousel-indicators">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<li data-target="#carousel_{{$key}}" data-slide-to="{{$k}}" class="{{ if eq $k "0"}} active {{end}}"></li>
|
||||
{{end}}
|
||||
</ol>
|
||||
<div class="carousel-inner">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<div class="carousel-item {{ if eq $k "0"}} active {{end}}">
|
||||
<img class="d-block w-100 stream-img fix-height" channel="{{$k}}" src="/../static/img/noimage.svg">
|
||||
<div class="carousel-caption d-none d-md-block">
|
||||
<h5>Channel: {{$k}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carousel_{{$key}}" role="button" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carousel_{{$key}}" role="button" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="btn-group stream">
|
||||
{{ if gt (len .Channels) 1}}
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> MSE</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/mse/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> HLS</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/hls/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> WebRTC</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/webrtc/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> ALL</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/all/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/mse/{{$key}}/0"><i class="fas fa-play"></i> MSE</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/hls/{{$key}}/0"><i class="fas fa-play"></i> HLS</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/webrtc/{{$key}}/0"><i class="fas fa-play"></i> WebRTC</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/all/{{$key}}/0"><i class="fas fa-play"></i> ALL</a>
|
||||
{{end}}
|
||||
<a class="btn btn-secondary btn-flat btn-xs" href="/pages/stream/edit/{{$key}}"><i class="fas fa-edit"></i> Edit</a>
|
||||
<a class="btn btn-danger btn-flat btn-xs" onclick="deleteStream('{{ $key }}')" href="#"><i class="fas fa-times"></i> Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "foot.tmpl" .}}
|
||||
</div>
|
||||
<script src="/../static/plugins/hlsjs/hls.min.js?version=3"></script>
|
||||
<script>
|
||||
let uuid=$('#uuid').val();
|
||||
let channel=$('#channel').val();
|
||||
let colordebug = true;
|
||||
let isSafari = navigator.vendor && navigator.vendor.indexOf('Apple') > -1 && navigator.userAgent && navigator.userAgent.indexOf('CriOS') == -1 && navigator.userAgent.indexOf('FxiOS') == -1;
|
||||
$(document).ready(()=>{
|
||||
$('.videoPlayer').each(function() {
|
||||
|
||||
switch ($(this).attr('vtype')) {
|
||||
case 'hls':
|
||||
var url = '/stream/' + uuid + '/channel/' + channel + '/hls/live/index.m3u8';
|
||||
if (isSafari && $(this)[0].canPlayType('application/vnd.apple.mpegurl')) {
|
||||
$(this)[0].src = url;
|
||||
$(this)[0].load();
|
||||
} else if (Hls.isSupported()) {
|
||||
let hlsplayer = new Hls({
|
||||
manifestLoadingTimeOut: 60000
|
||||
});
|
||||
hlsplayer.loadSource(url);
|
||||
hlsplayer.attachMedia($(this)[0]);
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Oops...',
|
||||
text: 'Your browser don`t support hls '
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'hlsll':
|
||||
var url = '/stream/' + uuid + '/channel/' + channel + '/hlsll/live/index.m3u8';
|
||||
if (isSafari && $(this)[0].canPlayType('application/vnd.apple.mpegurl')) {
|
||||
$(this)[0].src = url;
|
||||
$(this)[0].load();
|
||||
} else if (Hls.isSupported()) {
|
||||
let hlsplayer = new Hls({
|
||||
manifestLoadingTimeOut: 60000
|
||||
});
|
||||
hlsplayer.loadSource(url);
|
||||
hlsplayer.attachMedia($(this)[0]);
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Oops...',
|
||||
text: 'Your browser don`t support hls '
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'mse':
|
||||
var mse = new msePlayer(uuid, $(this).closest('div'), channel);
|
||||
mse.playMse()
|
||||
break;
|
||||
case 'webrtc':
|
||||
var webrtc = new WebRTCPlayer(uuid, $(this).closest('div'), channel);
|
||||
webrtc.playWebrtc()
|
||||
break;
|
||||
default:
|
||||
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function msePlayer(uuid, videoPlayerVar, channel) {
|
||||
this.ws = null,
|
||||
this.video = videoPlayerVar.find('video')[0],
|
||||
this.mseSourceBuffer = null,
|
||||
this.mse = null,
|
||||
this.mseQueue = [],
|
||||
this.mseStreamingStarted = false,
|
||||
this.uuid = uuid,
|
||||
this.channel = channel || 0;
|
||||
this.timeout = null;
|
||||
this.checktime = null;
|
||||
this.checktimecounter = 0;
|
||||
this.playMse = function() {
|
||||
let _this = this;
|
||||
this.mse = new MediaSource();
|
||||
this.video.src = window.URL.createObjectURL(this.mse);
|
||||
|
||||
let potocol = 'ws';
|
||||
if (location.protocol == 'https:') {
|
||||
potocol = 'wss';
|
||||
}
|
||||
|
||||
let ws_url = potocol + '://' + location.host + '/stream/' + this.uuid + '/channel/' + this.channel + '/mse?uuid=' + this.uuid + '&channel=' + this.channel;
|
||||
|
||||
this.mse.addEventListener('sourceopen', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: sourceopen');
|
||||
_this.ws = new WebSocket(ws_url);
|
||||
_this.ws.binaryType = "arraybuffer";
|
||||
_this.ws.onopen = function(event) {
|
||||
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[websocket]: connected');
|
||||
}
|
||||
|
||||
_this.ws.onclose = function(event) {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[websocket]: closed');
|
||||
if (_this.timeout != null) {
|
||||
clearInterval(_this.timeout);
|
||||
_this.timeout = null;
|
||||
}
|
||||
_this.timeout = setTimeout(() => {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[websocket]: timeouted func play');
|
||||
//play(uuid, videoPlayerVar.index(), channel, 'mse')
|
||||
}, 15000)
|
||||
|
||||
|
||||
}
|
||||
_this.ws.onerror = (e) => {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[websocket]: error');
|
||||
}
|
||||
_this.ws.onmessage = function(event) {
|
||||
_this.checkStalled();
|
||||
let data = new Uint8Array(event.data);
|
||||
if (data[3] == 24) {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[data]: init_file');
|
||||
}
|
||||
|
||||
if (data[0] == 9) {
|
||||
decoded_arr = data.slice(1);
|
||||
if (window.TextDecoder) {
|
||||
mimeCodec = new TextDecoder("utf-8").decode(decoded_arr);
|
||||
} else {
|
||||
mimeCodec = Utf8ArrayToStr(decoded_arr);
|
||||
}
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[codec]: ' + mimeCodec);
|
||||
//console.log(mimeCodec);
|
||||
_this.mseSourceBuffer = _this.mse.addSourceBuffer('video/mp4; codecs="' + mimeCodec + '"');
|
||||
_this.mseSourceBuffer.mode = "segments"
|
||||
_this.mseSourceBuffer.addEventListener("updateend", _this.pushPacket.bind(_this));
|
||||
|
||||
} else {
|
||||
_this.readPacket(event.data);
|
||||
}
|
||||
};
|
||||
}, false);
|
||||
|
||||
this.mse.addEventListener('sourceended', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: sourceended');
|
||||
})
|
||||
this.mse.addEventListener('sourceclose', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: sourceclose');
|
||||
})
|
||||
|
||||
this.mse.addEventListener('error', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: error');
|
||||
})
|
||||
this.mse.addEventListener('abort', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: abort');
|
||||
})
|
||||
this.mse.addEventListener('updatestart', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: updatestart');
|
||||
})
|
||||
this.mse.addEventListener('update', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: update');
|
||||
})
|
||||
this.mse.addEventListener('updateend', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: updateend');
|
||||
})
|
||||
this.mse.addEventListener('addsourcebuffer', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: addsourcebuffer');
|
||||
})
|
||||
this.mse.addEventListener('removesourcebuffer', function() {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[MSE]: removesourcebuffer');
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
this.readPacket = function(packet) {
|
||||
if (!this.mseStreamingStarted) {
|
||||
try {
|
||||
this.mseSourceBuffer.appendBuffer(packet);
|
||||
this.mseStreamingStarted = true;
|
||||
} catch (e) {
|
||||
logger(videoPlayerVar.index(),
|
||||
'readPacket error',
|
||||
'streams: ' + uuid,
|
||||
'channel: ' + channel);
|
||||
console.log(e);
|
||||
|
||||
//play(uuid, videoPlayerVar.index(), channel, 'mse');
|
||||
|
||||
} finally {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
this.mseQueue.push(packet);
|
||||
|
||||
if (!this.mseSourceBuffer.updating) {
|
||||
this.pushPacket();
|
||||
}
|
||||
},
|
||||
|
||||
this.pushPacket = function() {
|
||||
let _this = this;
|
||||
if (!_this.mseSourceBuffer.updating) {
|
||||
if (_this.mseQueue.length > 0) {
|
||||
packet = _this.mseQueue.shift();
|
||||
|
||||
try {
|
||||
_this.mseSourceBuffer.appendBuffer(packet)
|
||||
} catch (e) {
|
||||
logger(videoPlayerVar.index(),
|
||||
'pushPacket error',
|
||||
'streams: ' + uuid,
|
||||
'channel: ' + channel);
|
||||
console.log(e);
|
||||
|
||||
//play(uuid, videoPlayerVar.index(), channel, 'mse');
|
||||
} finally {
|
||||
|
||||
}
|
||||
} else {
|
||||
_this.mseStreamingStarted = false;
|
||||
}
|
||||
}
|
||||
if (_this.video.buffered.length > 0) {
|
||||
if (typeof document.hidden !== "undefined" && document.hidden) {
|
||||
_this.video.currentTime = _this.video.buffered.end((_this.video.buffered.length - 1)) - 0.5;
|
||||
} else {
|
||||
if ((_this.video.buffered.end((_this.video.buffered.length - 1)) - _this.video.currentTime) > 60) {
|
||||
_this.video.currentTime = _this.video.buffered.end((_this.video.buffered.length - 1)) - 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.checkStalled = function() {
|
||||
if (!!this.video.currentTime) {
|
||||
if (this.video.currentTime == this.checktime) {
|
||||
this.checktimecounter += 1;
|
||||
} else {
|
||||
this.checktimecounter = 0;
|
||||
}
|
||||
}
|
||||
if (this.checktimecounter > 10) {
|
||||
logger(videoPlayerVar.index(),
|
||||
uuid,
|
||||
channel,
|
||||
'[FIX]: player not move');
|
||||
//play(uuid, videoPlayerVar.index(), channel, 'mse');
|
||||
}
|
||||
this.checktime = this.video.currentTime;
|
||||
|
||||
}
|
||||
|
||||
this.destroy = function() {
|
||||
if (this.timeout != null) {
|
||||
clearInterval(this.timeout);
|
||||
}
|
||||
if (this.ws != null) {
|
||||
|
||||
this.ws.onclose = null;
|
||||
this.ws.close(1000, "stop streaming");
|
||||
}
|
||||
|
||||
|
||||
|
||||
logger(videoPlayerVar.index(),
|
||||
'Event: PlayerDestroy',
|
||||
'streams: ' + uuid,
|
||||
'channel: ' + channel);
|
||||
}
|
||||
this.video.addEventListener('pause', () => {
|
||||
if (this.video.currentTime > this.video.buffered.end((this.video.buffered.length - 1))) {
|
||||
this.video.currentTime = this.video.buffered.end((this.video.buffered.length - 1)) - 0.1;
|
||||
this.video.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
/*************************end mse obect **************************/
|
||||
/*************************WEBRTC obect **************************/
|
||||
function WebRTCPlayer(uuid, videoPlayerVar, channel) {
|
||||
this.webrtc = null;
|
||||
this.webrtcSendChannel = null;
|
||||
this.webrtcSendChannelInterval = null;
|
||||
this.uuid = uuid;
|
||||
this.video = videoPlayerVar.find('video')[0];
|
||||
this.channel = channel || 0;
|
||||
this.playWebrtc = function() {
|
||||
var _this = this;
|
||||
this.webrtc = new RTCPeerConnection({
|
||||
iceServers: [{
|
||||
urls: ["stun:stun.l.google.com:19302"]
|
||||
}]
|
||||
});
|
||||
this.webrtc.onnegotiationneeded = this.handleNegotiationNeeded.bind(this);
|
||||
this.webrtc.ontrack = function(event) {
|
||||
console.log(event.streams.length + ' track is delivered');
|
||||
_this.video.srcObject = event.streams[0];
|
||||
_this.video.play();
|
||||
}
|
||||
this.webrtc.addTransceiver('video', {
|
||||
'direction': 'sendrecv'
|
||||
});
|
||||
this.webrtcSendChannel = this.webrtc.createDataChannel('foo');
|
||||
this.webrtcSendChannel.onclose = (e) => console.log('sendChannel has closed', e);
|
||||
this.webrtcSendChannel.onopen = () => {
|
||||
console.log('sendChannel has opened');
|
||||
this.webrtcSendChannel.send('ping');
|
||||
this.webrtcSendChannelInterval = setInterval(() => {
|
||||
this.webrtcSendChannel.send('ping');
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
this.webrtcSendChannel.onmessage = e => console.log(e.data);
|
||||
},
|
||||
this.handleNegotiationNeeded = async function() {
|
||||
var _this = this;
|
||||
|
||||
offer = await _this.webrtc.createOffer();
|
||||
await _this.webrtc.setLocalDescription(offer);
|
||||
$.post("/stream/" + _this.uuid + "/channel/" + this.channel + "/webrtc?uuid=" + _this.uuid + "&channel=" + this.channel, {
|
||||
data: btoa(_this.webrtc.localDescription.sdp)
|
||||
}, function(data) {
|
||||
try {
|
||||
_this.webrtc.setRemoteDescription(new RTCSessionDescription({
|
||||
type: 'answer',
|
||||
sdp: atob(data)
|
||||
}))
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
this.destroy = function() {
|
||||
clearInterval(this.webrtcSendChannelInterval);
|
||||
this.webrtc.close();
|
||||
this.video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,78 @@
|
||||
{{template "head.tmpl" .}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">Play hls video</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Play hls video</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
</div>
|
||||
<div class="content">
|
||||
{{template "player.tmpl" .}}
|
||||
|
||||
{{template "foot.tmpl" .}}
|
||||
</div>
|
||||
<script src="/../static/plugins/hlsjs/hls.min.js"></script>
|
||||
<script>
|
||||
|
||||
$(document).ready(()=>{
|
||||
startPlay();
|
||||
});
|
||||
function startPlay(){
|
||||
let uuid=$('#uuid').val();
|
||||
let channel=$('#channel').val();
|
||||
let url ='/stream/' + uuid + '/channel/'+channel+'/hls/live/index.m3u8';
|
||||
if ($("#videoPlayer")[0].canPlayType('application/vnd.apple.mpegurl')) {
|
||||
$("#videoPlayer")[0].src = url;
|
||||
$("#videoPlayer")[0].load();
|
||||
} else if (Hls.isSupported()) {
|
||||
let hls=new Hls({manifestLoadingTimeOut:60000});
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia($("#videoPlayer")[0]);
|
||||
hls.on('error', function(e) {
|
||||
console.log(e);
|
||||
})
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Oops...',
|
||||
text: 'Your browser don`t support hls '
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
let canplaythroughTime=null;//time when buffer have enaugh to play
|
||||
|
||||
$("#videoPlayer")[0].addEventListener('progress', () => {
|
||||
if (typeof document.hidden !== "undefined" && document.hidden && canplaythroughTime!=null) {
|
||||
//no sound, browser paused video without sound in background
|
||||
$("#videoPlayer")[0].currentTime = $("#videoPlayer")[0].buffered.end(($("#videoPlayer")[0].buffered.length - 1)) - canplaythroughTime;
|
||||
}
|
||||
});
|
||||
|
||||
$("#videoPlayer")[0].addEventListener('canplaythrough', () => {
|
||||
if(canplaythroughTime==null){
|
||||
canplaythroughTime=$("#videoPlayer")[0].buffered.end(($("#videoPlayer")[0].buffered.length - 1));
|
||||
}
|
||||
});
|
||||
|
||||
$("#videoPlayer")[0].addEventListener('loadeddata', () => {
|
||||
$("#videoPlayer")[0].play();
|
||||
makePic();
|
||||
});
|
||||
|
||||
// $("#videoPlayer")[0].addEventListener('error', () => {
|
||||
// console.log('video_error')
|
||||
// });
|
||||
$("#videoPlayer")[0].onerror = function() {
|
||||
console.log("Error " + $("#videoPlayer")[0].error.code + "; details: " + $("#videoPlayer")[0].error.message);
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,114 @@
|
||||
{{template "head.tmpl" .}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">Play mse video</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Play mse video</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
</div>
|
||||
<div class="content">
|
||||
{{template "player.tmpl" .}}
|
||||
|
||||
{{template "foot.tmpl" .}}
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
startPlay();
|
||||
});
|
||||
|
||||
let mseQueue = [],
|
||||
mseSourceBuffer, mseStreamingStarted = false, videoSound=false;
|
||||
|
||||
function startPlay() {
|
||||
let uuid = $('#uuid').val();
|
||||
let channel = $('#channel').val();
|
||||
location.protocol == 'https:' ? potocol = 'wss' : potocol = 'ws';
|
||||
let url = potocol + '://' + location.host + '/stream/' + uuid + '/channel/' + channel + '/mse?uuid=' + uuid + '&channel=' + channel;
|
||||
let mse = new MediaSource();
|
||||
$("#videoPlayer")[0].src = window.URL.createObjectURL(mse);
|
||||
mse.addEventListener('sourceopen', function() {
|
||||
let ws = new WebSocket(url);
|
||||
ws.binaryType = "arraybuffer";
|
||||
ws.onopen = function(event) {
|
||||
console.log('Connect to ws');
|
||||
}
|
||||
ws.onmessage = function(event) {
|
||||
let data = new Uint8Array(event.data);
|
||||
if (data[0] == 9) {
|
||||
decoded_arr = data.slice(1);
|
||||
if (window.TextDecoder) {
|
||||
mimeCodec = new TextDecoder("utf-8").decode(decoded_arr);
|
||||
} else {
|
||||
mimeCodec = Utf8ArrayToStr(decoded_arr);
|
||||
}
|
||||
if(mimeCodec.indexOf(',')>0){
|
||||
videoSound=true;
|
||||
}
|
||||
mseSourceBuffer = mse.addSourceBuffer('video/mp4; codecs="' + mimeCodec + '"');
|
||||
mseSourceBuffer.mode = "segments"
|
||||
mseSourceBuffer.addEventListener("updateend", pushPacket);
|
||||
|
||||
} else {
|
||||
readPacket(event.data);
|
||||
}
|
||||
};
|
||||
}, false);
|
||||
|
||||
}
|
||||
|
||||
function pushPacket() {
|
||||
if (!mseSourceBuffer.updating) {
|
||||
if (mseQueue.length > 0) {
|
||||
packet = mseQueue.shift();
|
||||
mseSourceBuffer.appendBuffer(packet);
|
||||
} else {
|
||||
mseStreamingStarted = false;
|
||||
}
|
||||
}
|
||||
if ($("#videoPlayer")[0].buffered.length > 0) {
|
||||
if (typeof document.hidden !== "undefined" && document.hidden && !videoSound) {
|
||||
//no sound, browser paused video without sound in background
|
||||
$("#videoPlayer")[0].currentTime = $("#videoPlayer")[0].buffered.end(($("#videoPlayer")[0].buffered.length - 1)) - 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readPacket(packet) {
|
||||
if (!mseStreamingStarted) {
|
||||
mseSourceBuffer.appendBuffer(packet);
|
||||
mseStreamingStarted = true;
|
||||
return;
|
||||
}
|
||||
mseQueue.push(packet);
|
||||
if (!mseSourceBuffer.updating) {
|
||||
pushPacket();
|
||||
}
|
||||
}
|
||||
|
||||
$("#videoPlayer")[0].addEventListener('loadeddata', () => {
|
||||
$("#videoPlayer")[0].play();
|
||||
let browser = browserDetector();
|
||||
if (!browser.safari) {
|
||||
makePic();
|
||||
}
|
||||
});
|
||||
//fix stalled video in safari
|
||||
$("#videoPlayer")[0].addEventListener('pause', () => {
|
||||
if ($("#videoPlayer")[0].currentTime > $("#videoPlayer")[0].buffered.end(($("#videoPlayer")[0].buffered.length - 1))) {
|
||||
$("#videoPlayer")[0].currentTime = $("#videoPlayer")[0].buffered.end(($("#videoPlayer")[0].buffered.length - 1)) - 0.1;
|
||||
$("#videoPlayer")[0].play();
|
||||
}
|
||||
});
|
||||
|
||||
$("#videoPlayer")[0].addEventListener('error', () => {
|
||||
console.log('video_error')
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,128 @@
|
||||
{{template "head.tmpl" .}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">Play webrtc video</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Play webrtc video</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
</div>
|
||||
<div class="content">
|
||||
{{template "player.tmpl" .}}
|
||||
|
||||
{{template "foot.tmpl" .}}
|
||||
</div>
|
||||
<script>
|
||||
let webrtc, webrtcSendChannel;
|
||||
let mediaStream;
|
||||
|
||||
$(document).ready(() => {
|
||||
startPlay();
|
||||
});
|
||||
|
||||
async function startPlay() {
|
||||
mediaStream = new MediaStream();
|
||||
$("#videoPlayer")[0].srcObject = mediaStream;
|
||||
webrtc = new RTCPeerConnection({
|
||||
iceServers: [{
|
||||
urls: ["stun:stun.l.google.com:19302"]
|
||||
}],
|
||||
sdpSemantics: "unified-plan"
|
||||
});
|
||||
webrtc.onnegotiationneeded = handleNegotiationNeeded;
|
||||
webrtc.onsignalingstatechange = signalingstatechange;
|
||||
|
||||
webrtc.ontrack = ontrack
|
||||
let offer = await webrtc.createOffer({
|
||||
//iceRestart:true,
|
||||
offerToReceiveAudio:true,
|
||||
offerToReceiveVideo:true
|
||||
});
|
||||
await webrtc.setLocalDescription(offer);
|
||||
}
|
||||
|
||||
function ontrack (event){
|
||||
console.log(event.streams.length + ' track is delivered');
|
||||
mediaStream.addTrack(event.track);
|
||||
}
|
||||
|
||||
async function signalingstatechange (){
|
||||
switch (webrtc.signalingState){
|
||||
case 'have-local-offer':
|
||||
let uuid = $('#uuid').val();
|
||||
let channel = $('#channel').val();
|
||||
let url = "/stream/" + uuid + "/channel/" + channel + "/webrtc?uuid=" + uuid + '&channel=' + channel;
|
||||
$.post(url, {
|
||||
data: btoa(webrtc.localDescription.sdp)
|
||||
}, function(data) {
|
||||
try {
|
||||
console.log(data);
|
||||
webrtc.setRemoteDescription(new RTCSessionDescription({
|
||||
type: 'answer',
|
||||
sdp: atob(data)
|
||||
}))
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
});
|
||||
break;
|
||||
case 'stable':
|
||||
/*
|
||||
* There is no ongoing exchange of offer and answer underway.
|
||||
* This may mean that the RTCPeerConnection object is new, in which case both the localDescription and remoteDescription are null;
|
||||
* it may also mean that negotiation is complete and a connection has been established.
|
||||
*/
|
||||
break;
|
||||
|
||||
case 'closed':
|
||||
/*
|
||||
* The RTCPeerConnection has been closed.
|
||||
*/
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`unhandled signalingState is ${webrtc.signalingState}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNegotiationNeeded() {
|
||||
let uuid = $('#uuid').val();
|
||||
let channel = $('#channel').val();
|
||||
let url = "/stream/" + uuid + "/channel/" + channel + "/webrtc?uuid=" + uuid + '&channel=' + channel;
|
||||
let offer = await webrtc.createOffer();
|
||||
|
||||
await webrtc.setLocalDescription(offer);
|
||||
$.post(url, {
|
||||
data: btoa(webrtc.localDescription.sdp)
|
||||
}, function(data) {
|
||||
try {
|
||||
console.log(data);
|
||||
webrtc.setRemoteDescription(new RTCSessionDescription({
|
||||
type: 'answer',
|
||||
sdp: atob(data)
|
||||
}))
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
$("#videoPlayer")[0].addEventListener('loadeddata', () => {
|
||||
$("#videoPlayer")[0].play();
|
||||
makePic();
|
||||
});
|
||||
|
||||
$("#videoPlayer")[0].addEventListener('error', () => {
|
||||
console.log('video_error')
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,116 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- player -->
|
||||
<div class="col-12 col-md-7 col-lg-8">
|
||||
<div class="card" style="position: sticky; top: 20px;">
|
||||
<div class="card-body p-0">
|
||||
<video id="videoPlayer" autoplay controls muted playsinline></video>
|
||||
<canvas id="canvas" class="d-none"></canvas>
|
||||
</div>
|
||||
<input type="hidden" id="uuid" value="{{ .uuid }}" />
|
||||
<input type="hidden" id="channel" value="{{ .channel }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5 col-lg-4">
|
||||
<div class="row">
|
||||
{{ range $key, $value := .streams }}
|
||||
<div class="col-12" id="{{ $key }}">
|
||||
|
||||
<div class="card card-outline card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title one-line-header">{{.Name}}</h3>
|
||||
<div class="card-tools">
|
||||
<span data-toggle="tooltip" title="avaliable channels" class="badge badge-success">{{len .Channels }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
|
||||
<div id="carousel_{{$key}}" class="carousel slide" data-ride="carousel">
|
||||
<ol class="carousel-indicators">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<li data-target="#carousel_{{$key}}" data-slide-to="{{$k}}" class="{{ if eq $k "0"}} active {{end}}"></li>
|
||||
{{end}}
|
||||
</ol>
|
||||
<div class="carousel-inner">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<div class="carousel-item {{ if eq $k "0"}} active {{end}}">
|
||||
<img class="d-block w-100 stream-img fix-height" channel="{{$k}}" src="/../static/img/noimage.svg">
|
||||
<div class="carousel-caption d-none d-md-block">
|
||||
<h5>Channel: {{$k}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carousel_{{$key}}" role="button" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carousel_{{$key}}" role="button" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="btn-group stream">
|
||||
{{ if gt (len .Channels) 1}}
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> MSE</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/mse/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> HLS</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/hls/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> WebRTC</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/webrtc/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> ALL</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/all/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/mse/{{$key}}/0"><i class="fas fa-play"></i> MSE</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/hls/{{$key}}/0"><i class="fas fa-play"></i> HLS</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/webrtc/{{$key}}/0"><i class="fas fa-play"></i> WebRTC</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/all/{{$key}}/0"><i class="fas fa-play"></i> ALL</a>
|
||||
{{end}}
|
||||
|
||||
|
||||
|
||||
<a class="btn btn-secondary btn-flat btn-xs" href="/pages/stream/edit/{{$key}}"><i class="fas fa-edit"></i> Edit</a>
|
||||
<a class="btn btn-danger btn-flat btn-xs" onclick="deleteStream('{{ $key }}')" href="#"><i class="fas fa-times"></i> Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
{{template "head.tmpl" .}}
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0 text-dark">Streams list</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Streams list</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.container-fluid -->
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row mt-3 ">
|
||||
{{ range $key, $value := .streams }}
|
||||
<div class="col-12 col-sm-6 col-md-3" id="{{ $key }}">
|
||||
|
||||
<div class="card card-outline card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title one-line-header">{{.Name}}</h3>
|
||||
<div class="card-tools">
|
||||
<span data-toggle="tooltip" title="avaliable channels" class="badge badge-success">{{len .Channels }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
|
||||
<div id="carousel_{{$key}}" class="carousel slide" data-ride="carousel">
|
||||
<ol class="carousel-indicators">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<li data-target="#carousel_{{$key}}" data-slide-to="{{$k}}" class="{{ if eq $k "0"}} active {{end}}"></li>
|
||||
{{end}}
|
||||
</ol>
|
||||
<div class="carousel-inner">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<div class="carousel-item {{ if eq $k "0"}} active {{end}}">
|
||||
<img class="d-block w-100 stream-img fix-height" channel="{{$k}}" src="/../static/img/noimage.svg">
|
||||
<div class="carousel-caption d-none d-md-block">
|
||||
<h5>Channel: {{$k}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carousel_{{$key}}" role="button" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carousel_{{$key}}" role="button" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="btn-group stream">
|
||||
{{ if gt (len .Channels) 1}}
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> MSE</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/mse/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> HLS</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/hls/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> WebRTC</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/webrtc/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group-prepend">
|
||||
<a class="btn btn-info btn-flat btn-xs" data-toggle="dropdown" href="#"><i class="fas fa-play"></i> ALL</a>
|
||||
<div class="dropdown-menu">
|
||||
{{ range $k, $v := .Channels }}
|
||||
<a class="dropdown-item" href="/pages/player/all/{{$key}}/{{$k}}">Channel {{$k}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/mse/{{$key}}/0"><i class="fas fa-play"></i> MSE</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/hls/{{$key}}/0"><i class="fas fa-play"></i> HLS</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/webrtc/{{$key}}/0"><i class="fas fa-play"></i> WebRTC</a>
|
||||
<a class="btn btn-info btn-flat btn-xs" href="/pages/player/all/{{$key}}/0"><i class="fas fa-play"></i> ALL</a>
|
||||
{{end}}
|
||||
|
||||
|
||||
|
||||
<a class="btn btn-secondary btn-flat btn-xs" href="/pages/stream/edit/{{$key}}"><i class="fas fa-edit"></i> Edit</a>
|
||||
<a class="btn btn-danger btn-flat btn-xs" onclick="deleteStream('{{ $key }}')" href="#"><i class="fas fa-times"></i> Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{template "foot.tmpl" .}}
|
||||
Reference in new issue
Block a user