Comments

1 comment

  • Avatar
    Tagir Gadelshin

    Lukas Zich

    we will add this in some future. the first issue we are dealing with is a big latency of our HLS-based web player. But we have WebRTC support in development and latency issue should be fixed soon.

    In the meanwhile, we can only suggest some local workaround, that I'll describe below.

    I've attached a javascript code that can control ptz cameras with a web browser bookmark.
    The code below needs to be copied into the url section of a web browser bookmark. Then just click this bookmark when PTZ camera is opened on View tab. It will trigger this js code and it will show PTZ control overlay.
    javascript: (async () => {
    	const auth = {};
    	const systems = {};
    	let isCloud = false;
    	let panel;
    	let relay;
    
    	/* Start of generic request helpers */
    	const _getCsrf = () => {
    		let csrf;
    		try {
    			csrf = document.cookie?.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1];
    		} catch (_) {
    			csrf = '';
    		}
    		return csrf;
    	};
    
    	const get = (url, headers) => {
    		headers = new Headers(headers || {});
    		headers.set('accept', 'application/json, text/plain, */*');
    		headers.set('content-type', 'application/json');
    		return fetch(url, { headers })
    			.then(r => r.json());
    	};
    
    	const post = (url, headers, body) => {
    		body = JSON.stringify(body || {});
    		headers = new Headers(headers || {});
    		headers.set('accept', 'application/json, text/plain, */*');
    		headers.set('content-type', 'application/json');
    
    		let csrf = _getCsrf();
    		if (csrf) {
    			headers.set('X-CSRFToken', csrf);
    		}
    		return fetch(url, { body, headers, method: 'POST' })
    			.then(r => r.json());
    	};
    	/* End of generic request helpers */
    
    
    	/*Start of cloud api calls*/
    	const _setAuth = async ({id, isRest}) => {
    		if (!(id in auth)) {
    			auth[id] = await (isRest ?
    				post(`/api/systems/${id}/token`).then(({ access_token }) => access_token) :
    				get(`/api/systems/${id}/auth`));
    		}
    	};
    
    	const _addParamsToUrl = (url, params) => {
    		const origin = window.location.origin;
    		url = new URL(url, origin);
    		url.search = params;
    		return url.toString().replace(origin, '');
    	};
    
    	const _buildHeadersAndParams = (system, params, method) => {
    		headers = {};
    		params = new URLSearchParams(params || {});
    		if (system.isRest) {
    			headers = {"x-runtime-guid": auth[system.id] || ''};
    		} else {
    			const key = method === 'POST' ? auth[system.id].authPost : auth[system.id].authGet;
    			params.set('auth', key || '');
    		}
    		return { headers, params };
    	};
    
    	const _getRelay = (system, url, params) => {
    		const res =  _buildHeadersAndParams(system, params, 'GET');
    		return get(`${relay.replace('{systemId}', system.id)}${_addParamsToUrl(url, res.params.toString())}`, res.headers)
    			.catch(() => {
    				delete auth[system.id];
    				alert(`Request failed for ${system.name}(${system.id})`);
    			});
    	};
    
    	const _postRelay = (system, url, body) => {
    		const res = _buildHeadersAndParams(system, {}, 'POST');
    		return post(`${relay.replace('{systemId}', system.id)}${_addParamsToUrl(url, res.params.toString())}`, res.headers, body)
    			.catch(() => {
    				delete auth[system.id];
    				alert(`Request failed for ${system.name}(${system.id})`);
    			});
    	};
    
    	const getRelay = async () => {
    		const settings = await get('/api/utils/settings');
    		relay = `https://${settings?.trafficRelayHost}`;
    		if (!settings) {
    			return false;
    		}
    		return true;
    	};
    	/*End of cloud api calls*/
    
    
    	/* Project api calls */
    
    	const _getToken = () => {
    		try {
    			return document.cookie?.split('; ').find(row => row.startsWith('x-runtime-guid='))?.split('=')[1];
    		} catch(_) {
    			return '';
    		}
    	};
    
    	const move = async (direction) => {
    		const rotation = 0.25;
    		const cameraId = window.location.href.match(/.*\/view\/([\d\w-]+)(?:.*)?/)?.[1];
    		if (!cameraId) {
    			alert('Couldn\'t parse camera id from the url');
    			return;
    		}
    		let x = 0;
    		let y = 0;
    		switch(direction) {
    			case 'up':
    				y = rotation;
    				break;
    			case 'down':
    				y = -1 * rotation;
    				break;
    			case 'left':
    				x = -1 * rotation;
    				break;
    			case 'right':
    				x = rotation;
    				break;
    			default:
    				console.log('no matching direction');
    				return;
    		}
    		const url = '/api/ptz';
    		const params = {
    			command: 'RelativeMovePtzCommand',
    			pan: x,
    			tilt: y,
    			speed: 0.2,
    			cameraId: `{${cameraId}}`
    		};
    		if (isCloud) {
    			const id = window.location.href.match(/.*\/systems\/([\w\d-]+)\/.*/)?.[1];
    			if (!id) {
    				alert('Couldn\'t parse system id from the url');
    				return;
    			}
    			let system;
    			if (!(id in auth)) {
    				system = (await get(`/api/systems/${id}`))?.[0];
    				if(!system) {
    					alert('Couldn\'t get system info');
    					return;
    				}
    				system.isRest = parseInt(system.version[0]) > 4;
    				systems[id] = system;
    			} else {
    				system = systems[id];
    			}
    			await _setAuth(system);
    			return _getRelay(system, url, params).then(r => {
    				console.log(`Camera has rotated ${direction}`);
    			});
    		}
    		const token = _getToken();
    		const paramObject = new URLSearchParams(params || {});
    		return fetch(_addParamsToUrl(url, paramObject.toString()), {headers: {"x-runtime-guid": token}})
    			.then(r => r.json())
    			.then(r => {
    				console.log(`Camera has rotated ${direction}`);
    			}).catch(e => console.log(e));
    	};
    
    	const initApi = async() => {
    		const r = await get('/api/ping');
    		isCloud = !!r.realm;
    		if(isCloud) {
    			await getRelay();
    		}
    	};
    	/* End of project api calls */
    
    	/* Start of UI functions */
    	const dragAbleParentElement = (elmnt) => {
    		const closeDragElement = () => {
    			document.onmouseup = null;
    			document.onmousemove = null;
    		};
    
    		const elementDrag = (e) => {
    			e = e || window.event;
    			e.preventDefault();
    			pos1 = pos3 - e.clientX;
    			pos2 = pos4 - e.clientY;
    			pos3 = e.clientX;
    			pos4 = e.clientY;
    			elmnt.parentElement.style.top = (elmnt.parentElement.offsetTop - pos2) + "px";
    			elmnt.parentElement.style.left = (elmnt.parentElement.offsetLeft - pos1) + "px";
    		};
    
    		const dragMouseDown = (e) => {
    			e = e || window.event;
    			e.preventDefault();
    			pos3 = e.clientX;
    			pos4 = e.clientY;
    			document.onmouseup = closeDragElement;
    			document.onmousemove = elementDrag;
    		};
    
    		let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
    		elmnt.onmousedown = dragMouseDown;
    	};
    
    	const makePanel = () => {
    		panel = document.createElement('div');
    		panel.className = 'card';
    		panel.innerHTML =
    		`<div class="card--header w-100">
    			<h2 class="w-100" style="justify-content: center">PTZ MVP</h2>
    			<button id="ptz-close" class="close" aria-label="close" style="right: 0">
    				<div class="close-content">
    					<span class="close-icon"></span>
    				</div>
    			</button>
    		</div>
    		<div class="card--body w-100 d-flex" style="flex-direction: column; align-items: center;">
    			<div>
    				<button onclick="move('up');">up</button>
    			</div>
    			<div class="d-flex w-100" style="justify-content: space-between;">
    				<button onclick="move('left');">left</button>
    				<button onclick="move('right');">right</button>
    			</div>
    			<div>
    				<button onclick="move('down');">down</button>
    			</div>
    		</div>`;
    		Object.assign(panel.style, {
    			position: 'absolute',
    			top: '3rem',
    			right: '1rem',
    			zIndex: '1000',
    			maxHeight: '300px',
    			width: '300px',
    			overflow: 'scroll',
    			background: 'var(--additional_light2)',
    			display: 'flex',
    			flexDirection: 'column',
    			justifyContent: 'center',
    			alignItems: 'center'
    		});
    
    		document.body.append(panel);
    		const close = document.getElementById('ptz-close');
    		close.onclick = () => {
    			document.body.removeChild(panel);
    		};
    		window.move = move;
    	};
    
    	const initUI = () => {
    		makePanel();
    		dragAbleParentElement(panel.firstChild);
    	};
    	/* End of UI functions */
    	await initApi();
    	initUI();
    })();


    Please, note -- this functionality is very simple MVP of PTZ controls and works only inside your browser. Also, we haven't tested it extensively, so it may not work as expected for all cameras out there.

     

    thanks

    0
    Comment actions Permalink

Please sign in to leave a comment.