
const FORMAT_JPEG=0;
const FORMAT_H26X=1;
const TYPE_JPEG=0;
const TYPE_H264=1;
const TYPE_H265=2;
const TYPE_ULAW=128;
const TYPE_AAC=129;
const TYPE_LIMIT=253;
const TYPE_ERR=254;
const TYPE_XDATA=255;
const INFO_HEIGHT=14;
const OP_SIZE=1;
const OP_PLAY_PAUSE=2;
const OP_SCHED=3;
const OP_OVERR=4;
const OP_AUDIO=5;
const CROP_MODE_BARS=0;
const CROP_MODE_CROP=1;
const CROP_MODE_STRETCH=2;
const CODEC_H264='avc1.42E01E';
const CODEC_H265='hev1.1.6.L93.B0';
class Player{
constructor(cam,parentDivId,format,fps,clickHandler,auth){
let mseAvailable=('MediaSource' in window);
this.cam=cam;
this.format=mseAvailable?format:FORMAT_JPEG;
this.h265Available=(mseAvailable && MediaSource.isTypeSupported(`video/mp4; codecs="${CODEC_H265}"`));
this.fps=fps;
this.sizeFraction=0;
this.sock=null;
this.imgURL=null;
this.audioPlayer=null;
this.audioCapture=null;
this.audioCaptureSampleRate=16000;
this.scheds={c:0,m:0,a:0};
this.overrides={c:0,m:0,a:0};
this.buffer=[];
this.auth=auth;
this.isLimitReached=false;
this.zoom=1;
this.panX=0;
this.panY=0;
this.cropMode=CROP_MODE_BARS;
this.hiRes=false;
this.autoPatrolStatus=null;
this.playerDiv=AddElement(document.getElementById(parentDivId),'div');
this.playerDiv.className='playerDiv';
this.infoSpan=AddElement(this.playerDiv,'span');
this.infoSpan.textContent=cam.name;
this.infoSpan.className='infoSpan';
if(clickHandler)this.infoSpan.onclick=function(){clickHandler(cam,false)};
if(clickHandler)this.infoSpan.ondblclick=function(){clickHandler(cam,true)};
let pos=0;
this.more=AddElement(this.infoSpan,'span','right:'+pos+'px;');pos+=16;
this.more.className='moreSpan';
this.more.textContent='\u2026';
this.more.onclick=(event)=>{
let menu=new PopMenu(event);
menu.addItem(Loc('Remove From View'),false,()=>{
document.getElementById('liveCam'+cam.num).checked=false;
LiveCamSelectionChanged();
});
if(cam.perm&PERM_FILES){
menu.addItem(Loc('View Captures'),false,()=>{
_cap.pendingCam=cam.num;
document.getElementById('mainMenu').value='cap';
SectionChange();
});
}
if(_isAdmin){
menu.addItem(Loc('Configure Settings'),false,()=>{
_sg.pendingCam=cam.num;
document.getElementById('mainMenu').value='set';
SectionChange();
});
}
if(cam.shortcuts){
for(let item of cam.shortcuts){
menu.addItem(item.name,false,()=>{
fetch('doShortcut?id='+item.id);
});
}
}
event.stopPropagation();
};
if(cam.perm&PERM_SCHED)
{
this.a=AddElement(this.infoSpan,'div','right:'+pos+'px;');pos+=18;
this.m=AddElement(this.infoSpan,'div','right:'+pos+'px;');pos+=18;
this.c=AddElement(this.infoSpan,'div','right:'+pos+'px;');pos+=20;
this.a.className='cma';
this.m.className='cma';
this.c.className='cma';
this.a.textContent='A';
this.m.textContent='M';
this.c.textContent='C';
this.a.onclick=(event)=>{this.modeClick(event,'a');event.stopPropagation();}
this.m.onclick=(event)=>{this.modeClick(event,'m');event.stopPropagation();}
this.c.onclick=(event)=>{this.modeClick(event,'c');event.stopPropagation();}
this.a.oncontextmenu=(event)=>{this.modeClick(event,'a');event.stopPropagation();};
this.m.oncontextmenu=(event)=>{this.modeClick(event,'m');event.stopPropagation();};
this.c.oncontextmenu=(event)=>{this.modeClick(event,'c');event.stopPropagation();};
}
if(cam.audior && cam.perm&PERM_AUDIORCV){
this.speaker=AddElement(this.infoSpan,'img','right:'+pos+'px;');pos+=14;
this.speaker.src='../img/speaker-white.png';
this.speaker.dataset.mute='1';
this.speaker.className='audioButton';
this.speaker.onclick=(e)=>{
this.toggleSpeaker();
event.stopPropagation();
};
}
if(cam.audios && cam.perm&PERM_AUDIOSND){
this.mic=AddElement(this.infoSpan,'img','right:'+pos+'px;');pos+=14;
this.mic.src='../img/mic-white.png';
this.mic.dataset.mute='1';
this.mic.className='audioButton';
this.mic.onclick=(e)=>{
this.toggleMic();
event.stopPropagation();
};
}
this.zoomSpan=AddElement(this.infoSpan,'span','right:'+pos+'px;');
this.zoomSpan.className='zoomSpan';
if(this.format==FORMAT_H26X){
this.video=AddElement(this.playerDiv,'video');
this.video.type='video/mp4';
this.video.muted=true;
this.video.autoplay=true;
this.video.controls=false;
this.video.id='video'+cam.num;
if(clickHandler)this.video.onclick=function(){clickHandler(cam,false)};
if(clickHandler)this.video.ondblclick=function(){clickHandler(cam,true)};
this.video.addEventListener('error',(e)=>{
console.log(this.video.error);
if(this.sock)this.sock.close();
this.closeMediaSource();
});
this.video.addEventListener('wheel',(e)=>this.handleWheel(e),{passive:false});
}
if(this.format==FORMAT_JPEG){
this.img=AddElement(this.playerDiv,'img');
if(clickHandler)this.img.onclick=function(){clickHandler(cam,false)};
if(clickHandler)this.img.ondblclick=function(){clickHandler(cam,true)};
this.imgLoadHandler=()=>{
URL.revokeObjectURL(this.imgURL);
this.imgURL=null;
};
this.img.addEventListener('load',this.imgLoadHandler);
this.img.style.display='none';
this.img.addEventListener('wheel',(e)=>this.handleWheel(e),{passive:false});
}
this.interval=setInterval(()=>this.timer(),5000);
}
handleWheel(e){
let elem=this.img||this.video;
let isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform);
let modKey=isMac?e.metaKey:e.ctrlKey;
if(modKey){
e.preventDefault();
let rect=elem.getBoundingClientRect();
let mouseX=(e.clientX-rect.left)/rect.width;
let mouseY=(e.clientY-rect.top)/rect.height;
let oldZoom=this.zoom;
let delta=e.deltaY>0?-0.2:0.2;
this.zoom=Math.max(1,Math.min(10,this.zoom+delta));
if(this.zoom!=oldZoom){
let r=this.zoom/oldZoom;
this.panX=this.panX+(mouseX-0.5)*(1/this.zoom-1/oldZoom);
this.panY=this.panY+(mouseY-0.5)*(1/this.zoom-1/oldZoom);
this.constrainPan();
this.applyTransform();
let newSizeFraction=this.bestSizeFraction();
if(newSizeFraction!=this.sizeFraction && this.sock){
this.sizeFraction=newSizeFraction;
this.sendMessageToServer(OP_SIZE,newSizeFraction,(this.format==FORMAT_H26X && this.h265Available)?1:0,null);
}
}
}else if(this.zoom>1){
e.preventDefault();
this.panX-=e.deltaX/(elem.offsetWidth*this.zoom)*2;
this.panY-=e.deltaY/(elem.offsetHeight*this.zoom)*2;
this.constrainPan();
this.applyTransform();
}
}
constrainPan(){
let maxPan=(this.zoom-1)/this.zoom*0.5;
this.panX=Math.max(-maxPan,Math.min(maxPan,this.panX));
this.panY=Math.max(-maxPan,Math.min(maxPan,this.panY));
}
applyTransform(){
let elem=this.img||this.video;
elem.style.transform=`scale(${this.zoom}) translate(${this.panX*100}%,${this.panY*100}%)`;
elem.style.transformOrigin='center';
this.zoomSpan.style.display=this.zoom>1?'inline':'none';
this.zoomSpan.textContent=`${this.zoom.toFixed(1)}x`;
}
toggleSpeaker(){
this.speaker.dataset.mute=this.speaker.dataset.mute?'':'1';
this.speaker.src=this.speaker.dataset.mute?'../img/speaker-white.png':'../img/speaker-red.png';
this.sendMessageToServer(OP_PLAY_PAUSE,this.speaker.dataset.mute?0:1,1,null);
if(this.speaker.dataset.mute && this.audioPlayer){
this.audioPlayer.stop();
this.audioPlayer=null;
}
}
async toggleMic(){
if(this.audioCapture){
await this.audioCapture.stop();
this.audioCapture=null;
this.mic.dataset.mute='1';
this.mic.src='../img/mic-white.png';
}else{
this.audioCapture=new AudioCapture(this.audioCaptureSampleRate);
this.audioCapture.onData=(data)=>{
this.sendMessageToServer(OP_AUDIO,TYPE_ULAW,this.audioCaptureSampleRate,data);
};
try{
await this.audioCapture.start();
this.mic.dataset.mute='';
this.mic.src='../img/mic-red.png';
}catch(error){
await this.audioCapture.stop();
this.audioCapture=null;
}
}
}
openMediaSource(codec){
this.mediaSource=new MediaSource();
this.video.src=URL.createObjectURL(this.mediaSource);
this.mediaSource.addEventListener('sourceopen',()=>{
this.sourceBuffer=this.mediaSource.addSourceBuffer(`video/mp4; codecs="${codec}"`);
this.sourceBuffer.addEventListener('updateend',()=>{
if(!this.sourceBuffer.updating && this.buffer[0])this.sourceBuffer.appendBuffer(this.buffer.shift());
});
});
}
closeMediaSource(){
if(this.video && this.video.src)URL.revokeObjectURL(this.video.src);
this.mediaSource=null;
this.sourceBuffer=null;
this.buffer=[];
}
dispose(){
if(this.sock)this.sock.close();
if(this.img && this.imgLoadHandler)this.img.removeEventListener('load',this.imgLoadHandler);
this.playerDiv.remove();
if(this.interval)clearInterval(this.interval);
this.closeMediaSource();
}
setFrame(frame,camInfo,hiRes,cropMode,border){
const cropModes=['contain','cover','fill'];
this.cropMode=cropMode;
this.hiRes=hiRes;
this.infoSpan.style.display=(camInfo && frame.h>INFO_HEIGHT)?'block':'none';
let infoHeight=this.infoSpan.offsetHeight;
this.playerDiv.style.left=frame.x+'px';
this.playerDiv.style.top=frame.y+'px';
this.playerDiv.style.width=frame.w+'px';
this.playerDiv.style.height=frame.h+'px';
let e=(this.img?this.img:this.video);
e.style.height=(frame.h-infoHeight)+'px';
e.style.width=frame.w+'px';
e.style.objectFit=cropModes[cropMode];
let sizeFraction=this.bestSizeFraction();
if(this.sock){
if(sizeFraction!=this.sizeFraction){
this.sizeFraction=sizeFraction;
this.sendMessageToServer(OP_SIZE,sizeFraction,(this.format==FORMAT_H26X && this.h265Available)?1:0,null);
}
}else{
this.sizeFraction=sizeFraction;
this.startStream();
}
}
bestSizeFraction(){
let playerWidth=this.playerDiv.offsetWidth;
let playerHeight=this.playerDiv.offsetHeight;
let playerRatio=playerWidth/playerHeight;
let camRatio=this.cam.w/this.cam.h;
let imageWidth=playerWidth;
let imageHeight=playerHeight;
if(this.cropMode==CROP_MODE_BARS){
if(playerRatio>camRatio)imageWidth=Math.round(playerHeight*camRatio);
else imageHeight=Math.round(playerWidth/camRatio);
}else if(this.cropMode==CROP_MODE_CROP){
if(playerRatio>camRatio)imageHeight=Math.round(playerWidth/camRatio);
else imageWidth=Math.round(playerHeight*camRatio);
}
if(this.hiRes){
imageWidth*=2;
imageHeight*=2;
}
imageWidth*=this.zoom;
imageHeight*=this.zoom;
for(let div=16;div>1;div/=2)if(Math.round(this.cam.w/div)>=imageWidth && Math.round(this.cam.h)/div>=imageHeight)return div;
return 1;
}
timer(){
if(!this.sock)this.startStream();
if(this.video && this.video.currentTime>20 && this.sourceBuffer && !this.sourceBuffer.updating){
this.sourceBuffer.remove(0,this.video.currentTime-20);
}
}
startStream(){
if(this.sock || this.isLimitReached)return;
const vcodec=this.format?(this.h265Available?'h26x':'h264'):'jpeg';
const acodec=(this.cam.audior && this.cam.perm&PERM_AUDIORCV)?'ulaw':'';
const location=window.location;
const url=((location.protocol=='http:')?'ws://':'wss://')+location.hostname+':'+location.port+'/video?cameraNum='+this.cam.num+'&vcodec='+vcodec+'&acodec='+acodec+'&fps='+this.fps+'&apause=1&sizeFraction='+this.sizeFraction+'&auth='+this.auth;
this.sock=new WebSocket(url);
this.sock.binaryType='arraybuffer';
this.sock.onmessage=(msg)=>{this.newChunk(new Uint8Array(msg.data));};
this.sock.onclose=(msg)=>{this.sock=null;this.closeMediaSource();}
this.sock.onerror=(error)=>{if(this.sock)this.sock.close();};
}
newChunk(bytes){
const headerLen=bytes[0];
const type=bytes[1];
const val1=bytes[2];
const val2=bytes[3];
const timestamp=ToInt64(bytes[4],bytes[5],bytes[6],bytes[7],bytes[8],bytes[9],bytes[10],bytes[11])/600;
const err=ToInt32(bytes[12],bytes[13],bytes[14],bytes[15]);
const payloadData=err?null:bytes.subarray(headerLen);
if(err){
let sub=bytes.subarray(16,47);
let decoder=new TextDecoder('ascii');
let desc=decoder.decode(sub).replace(/\0+$/,'');
this.displayError(err,desc);
}else if(payloadData){
if(this.errP){this.errP.remove();this.errP=null;}
if(this.img)this.img.style.display='block';
if(this.video)this.video.style.display='block';
if(this.speaker)this.speaker.style.display='block';
if(this.mic)this.mic.style.display='block';
if(this.c)this.c.style.display='block';
if(this.m)this.m.style.display='block';
if(this.a)this.a.style.display='block';
}
if(err || !payloadData)return;
if(type==TYPE_JPEG){
if(!this.imgURL){
const blob=new Blob([payloadData],{type:'image/jpeg'});
this.imgURL=URL.createObjectURL(blob);
this.img.src=this.imgURL;
}
}else if(type==TYPE_H264 || type==TYPE_H265){
if(val1)this.closeMediaSource();
if(!this.mediaSource)this.openMediaSource((type==TYPE_H264)?CODEC_H264:CODEC_H265);
this.buffer.push(payloadData);
while(this.sourceBuffer && !this.sourceBuffer.updating && this.buffer[0]){
this.sourceBuffer.appendBuffer(this.buffer.shift());
}
if(this.video.currentTime<timestamp-4.0 || this.video.currentTime>timestamp){
this.video.currentTime=timestamp-1.0;
}
}else if(type==TYPE_ULAW){
if(this.speaker && !this.speaker.dataset.mute){
if(!this.audioPlayer)this.audioPlayer=new AudioPlayer(IndexToSampleRate(val1),val2,this.format?0.8:0.3);
this.audioPlayer.newData(payloadData);
}
}else if(type==TYPE_AAC){
}else if(type==TYPE_XDATA){
this.processXData(payloadData);
}else if(type==TYPE_LIMIT){
if(this.limitTimer)clearTimeout(this.limitTimer);
this.limitTimer=setTimeout(()=>{this.streamLimitReached();},timestamp*1000);
}
}
displayError(err,desc){
if(this.img)this.img.style.display='none';
if(this.video)this.video.style.display='none';
if(this.speaker)this.speaker.style.display='none';
if(this.mic)this.mic.style.display='none';
if(this.c)this.c.style.display='none';
if(this.m)this.m.style.display='none';
if(this.a)this.a.style.display='none';
if(this.sock){this.sock.close();this.sock=null;}
if(!_quietFail){
if(!this.errP)this.errP=AddElement(this.playerDiv,'p');
this.errP.innerHTML='<img src="img/disconnect.png"><br>'+(err?'Error '+err+' ':'')+(desc?desc:'');
}
}
streamLimitReached(){
this.limitTimer=null;
this.isLimitReached=true;
this.displayError(0,Loc('Stream Ended'));
}
processXData(data){
if(!this.c)return;
const cols=['#888','#e89235','#f01e1e'];
this.c.style.backgroundColor=cols[data[0]];
this.m.style.backgroundColor=cols[data[1]];
this.a.style.backgroundColor=cols[data[2]];
this.overrides.c=data[4];
this.overrides.m=data[5];
this.overrides.a=data[6];
this.scheds.c=ToInt16(data[8],data[9]);
this.scheds.m=ToInt16(data[10],data[11]);
this.scheds.a=ToInt16(data[12],data[13]);
this.c.style.display='block';
this.m.style.display='block';
this.a.style.display='block';
this.autoPatrolStatus=data[3];
}
modeClick(event,which){
try{
let menu=new PopMenu(event);
menu.addSectionTitle(Loc('Schedule'));
for(let o of document.getElementById('ssSchedSelect').options){
let scheduleId=parseInt(o.value);
menu.addItem(o.textContent,(this.scheds[which]==scheduleId)?true:false,()=>{this.setSchedule(scheduleId,which);});
}
menu.addSectionTitle(Loc('Override'));
for(let o of document.getElementById('ssOverrideSelect').options){
let overrideId=parseInt(o.value);
if(overrideId>-1)menu.addItem(o.textContent,(this.overrides[which]==overrideId)?true:false,()=>{this.setOverride(overrideId,which);});
}
}catch(error){}
}
setSchedule(id,which){
this.sendMessageToServer(OP_SCHED,which.charCodeAt(0),id,null);
}
setOverride(id,which){
this.sendMessageToServer(OP_OVERR,which.charCodeAt(0),id,null);
}
sendMessageToServer(opcode,val1,val2,payload){
if(this.sock && this.sock.readyState===WebSocket.OPEN){
const bytes=new Uint8Array(4+(payload?payload.length:0));
bytes[0]=opcode;
bytes[1]=val1;
bytes[2]=(val2>>8)& 0xff;
bytes[3]= val2 & 0xff;
if(payload)bytes.set(payload,4);
this.sock.send(bytes);
}
}
show(){
if(this.playerDiv.style.display=='none'){
this.playerDiv.style.display='';
this.sendMessageToServer(OP_PLAY_PAUSE,(!this.speaker || this.speaker.dataset.mute)?0:1,1,null);
}
}
hide(){
if(this.playerDiv.style.display!='none'){
this.playerDiv.style.display='none';
this.sendMessageToServer(OP_PLAY_PAUSE,0,(this.format==FORMAT_H26X)?1:0,null);
}
}
}
