You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
596 lines
20 KiB
596 lines
20 KiB
{{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>
|
|
|