// ==UserScript== // @name PTZ Web Panel Enhancer // @namespace https://dekvited.com // @version 1.1.0 // @description Add style and functionality enhancement for the Minrray PTZ web panel that controls it remotely // @author totymedli // @icon https://raw.githubusercontent.com/dekvidet/ptz-web-panel-enhancer/main/icon.png // @source https://github.com/dekvidet/ptz-web-panel-enhancer // @supportURL https://github.com/dekvidet/ptz-web-panel-enhancer // @updateURL https://raw.githubusercontent.com/dekvidet/ptz-web-panel-enhancer/main/script.user.js // @downloadURL https://raw.githubusercontent.com/dekvidet/ptz-web-panel-enhancer/main/script.user.js // @match http://*/pages/main.asp // @grant none // ==/UserScript== const CAMERA_NUMBER_OPTION_ID_BASE = '_easyui_combobox_i1_' const GO_TO_POSITION_BUTTON_ID = 'prerun' const SAVE_POSITION_BUTTON_ID = 'preset' const SPEED_SLIDER_ID = 'ptzSpeed' const ZOOM_IN_BUTTON_ID = 'ptzZoomIn' const ZOOM_OUT_BUTTON_ID = 'ptzZoomOut' const MOVE_UP_BUTTON_ID = 'ptzUp' const MOVE_DOWN_BUTTON_ID = 'ptzDown' const MOVE_LEFT_BUTTON_ID = 'ptzLeft' const MOVE_RIGHT_BUTTON_ID = 'ptzRight' const SPEED_AMOUNTS = [1, 10, 20, 25] const DELAY_BETWEEN_KEYPRESSES_IN_MS = 500 let isHotSwitching = true let isAutoPaning = false let isAutoPanLoopInProgress = false let autoPaningInterval = null let currentSpeedAmountIndex = 0 let autoPanRightTimeInMs = 2000 let autoPanLeftTimeInMs = 2000 setTimeout(() => { const mainWindow = document.getElementById('mainframe').contentWindow if (!mainWindow) { return } const mainDocument = mainWindow.document addHelp() addAutoPan() function changeSpeed(amount, vary) { const ptzSpeedelement = mainWindow.$(`#${SPEED_SLIDER_ID}`) const speed = ptzSpeedelement.slider('getValue') switch (vary) { case '+': ptzSpeedelement.slider('setValue', speed + amount) break case '-': ptzSpeedelement.slider('setValue', speed - amount) break default: ptzSpeedelement.slider('setValue', amount) } } function dispatch(id, eventName) { mainDocument.getElementById(id).dispatchEvent(new Event(eventName)) } function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function autoPanFor(panTimeInMs) { isAutoPanLoopInProgress = true if (isAutoPaning) { await wait(DELAY_BETWEEN_KEYPRESSES_IN_MS) dispatch(MOVE_RIGHT_BUTTON_ID, 'mousedown') await wait(panTimeInMs) dispatch(MOVE_RIGHT_BUTTON_ID, 'mouseup') } else { isAutoPanLoopInProgress = false return } if (isAutoPaning) { await wait(DELAY_BETWEEN_KEYPRESSES_IN_MS) dispatch(MOVE_LEFT_BUTTON_ID, 'mousedown') await wait(panTimeInMs) dispatch(MOVE_LEFT_BUTTON_ID, 'mouseup') } isAutoPanLoopInProgress = false } function addHelp() { const helpHtml = ` <div class="helpBtn"> <div class="helpWindow"> <ul> <li>0-9: Selects camera preset and moves camera to that position if hot switching is enabled.</li> <li>+, -: Increase/decrease camera move speed.</li> <li>*: Change between camera move speed of 1, 10, 20, 25.</li> <li>/: Toggle auto pan mode.</li> <li>Shift: Toggle hot switching mode. If turned on, changing the selected preset will also move the camera to that position. Turning it off is usefull when you would like to save the current camera position to a preset without moving the camera to the choosen preset.</li> <li>Space: Move the camera to the current preset position. Usefull when you turned off hot switching mode.</li> <li>Enter: Save camera position to current preset.</li> <li>↑, ↓, ←, →: Pan & tilt camera</li> <li>Page Up, Page Down: Zome in & out.</li> <li>Home: Set preset to 0 (home/bootup position) and move the camera to that position if hot switching is enabled.</li> </ul> </div> </div>` mainDocument.getElementById('rightpreview').insertAdjacentHTML('beforeend', helpHtml) } function addAutoPan() { const autoPanHtml = ` <td id="autoPanButtonCell"> <a id="autoPan" href="#" class="trackbtn autopan" title="Auto Pan"></a> </td> <td id="autoPanTimeCell"> <span> <input type="number" id="autoPanLeftTime" value="2000" />ms </span> <span> <input type="number" id="autoPanRightTime" value="2000" />ms </span> </td>` mainDocument.querySelector('#rightpreview .ptzdiv:nth-of-type(4) tr').insertAdjacentHTML('beforeend', autoPanHtml) } async function handleAutoPan(event) { if (isAutoPaning) { clearInterval(autoPaningInterval) autoPanButton.classList.remove('borderBlink') } else { autoPanButton.classList.add('borderBlink') dispatch(MOVE_LEFT_BUTTON_ID, 'mousedown') await wait(autoPanLeftTimeInMs) dispatch(MOVE_LEFT_BUTTON_ID, 'mouseup') autoPaningInterval = setInterval(async () => { if (isAutoPaning && !isAutoPanLoopInProgress) { console.log(autoPanLeftTimeInMs, autoPanRightTimeInMs) await autoPanFor(autoPanLeftTimeInMs + autoPanRightTimeInMs) } }, 50) } isAutoPaning = !isAutoPaning } console.log('PTZ Web Panel Enhancer started') const autoPanButton = mainDocument.getElementById('autoPan') autoPanButton.addEventListener('click', handleAutoPan) mainDocument.getElementById('autoPanLeftTime').addEventListener('change', event => { autoPanLeftTimeInMs = parseInt(event.target.value, 10) }); // Don't trigger preset load when entering value with number keys mainDocument.getElementById('autoPanLeftTime').addEventListener('keypress', event => event.stopPropagation()); mainDocument.getElementById('autoPanRightTime').addEventListener('change', event => { autoPanRightTimeInMs = parseInt(event.target.value, 10) }); // Don't trigger preset load when entering value with number keys mainDocument.getElementById('autoPanRightTime').addEventListener('keypress', event => event.stopPropagation()); mainDocument.addEventListener('keypress', event => { if (0 <= event.key && event.key <= 9) { mainDocument.getElementById(`${CAMERA_NUMBER_OPTION_ID_BASE}${event.key}`).click() if (isHotSwitching) { mainDocument.getElementById(GO_TO_POSITION_BUTTON_ID).click() } } }) mainDocument.addEventListener('keydown', event => { console.log(event.key) switch (event.key) { case 'Home': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(`${CAMERA_NUMBER_OPTION_ID_BASE}0`).click() if (isHotSwitching) { mainDocument.getElementById(GO_TO_POSITION_BUTTON_ID).click() } } break } case 'ArrowUp': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(MOVE_UP_BUTTON_ID).dispatchEvent(new Event('mousedown')) } break } case 'ArrowDown': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(MOVE_DOWN_BUTTON_ID).dispatchEvent(new Event('mousedown')) } break } case 'ArrowLeft': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(MOVE_LEFT_BUTTON_ID).dispatchEvent(new Event('mousedown')) } break } case 'ArrowRight': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(MOVE_RIGHT_BUTTON_ID).dispatchEvent(new Event('mousedown')) } break } case 'PageUp': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(ZOOM_IN_BUTTON_ID).dispatchEvent(new Event('mousedown')) } break } case 'PageDown': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(ZOOM_OUT_BUTTON_ID).dispatchEvent(new Event('mousedown')) } break } case '-': { event.preventDefault() changeSpeed(SPEED_AMOUNTS[currentSpeedAmountIndex], '-') break } case '+': { event.preventDefault() changeSpeed(SPEED_AMOUNTS[currentSpeedAmountIndex], '+') break } case '/': { event.preventDefault() currentSpeedAmountIndex = 0 break } case 'Shift': { event.preventDefault() if (!event.repeat) { isHotSwitching = !isHotSwitching } break } case '*': { event.preventDefault() ++currentSpeedAmountIndex if (currentSpeedAmountIndex >= SPEED_AMOUNTS.length) { currentSpeedAmountIndex = 0 } break } case '/': { event.preventDefault() handleAutoPan() break } case ' ': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(GO_TO_POSITION_BUTTON_ID).click() } break } case 'Enter': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(SAVE_POSITION_BUTTON_ID).click() } break } } }) mainDocument.addEventListener('keyup', event => { switch (event.key) { case 'ArrowUp': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(MOVE_UP_BUTTON_ID).dispatchEvent(new Event('mouseup')) } break } case 'ArrowDown': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(MOVE_DOWN_BUTTON_ID).dispatchEvent(new Event('mouseup')) } break } case 'ArrowLeft': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(MOVE_LEFT_BUTTON_ID).dispatchEvent(new Event('mouseup')) } break } case 'ArrowRight': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(MOVE_RIGHT_BUTTON_ID).dispatchEvent(new Event('mouseup')) } break } case 'PageUp': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(ZOOM_IN_BUTTON_ID).dispatchEvent(new Event('mouseup')) } break } case 'PageDown': { event.preventDefault() if (!event.repeat) { mainDocument.getElementById(ZOOM_OUT_BUTTON_ID).dispatchEvent(new Event('mouseup')) } break } } }) addGlobalStyle(` @keyframes borderBlink { from, to { border-color: transparent } 50% { border-color: red } } .borderBlink { animation: borderBlink 1s step-end infinite; border: 4px solid black !prevent-important; box-sizing: border-box; } .divPtz.panel-body.panel-body-noheader.panel-body-noborder.layout-body { min-width: 215px; } .head { display: none; } #rightpreview { display: flex; flex-direction: column; align-items: center; } #rightpreview > div:nth-child(4) tr { display: flex; flex-direction: column; align-items: center; } #rightpreview > div:nth-child(4) tr td:nth-child(5) { order: -4; } #rightpreview > div:nth-child(4) tr td:nth-child(2) { order: -3; } #autoPanButtonCell { order: -2; } #autoPanTimeCell { order: -1; } #rightpreview > div:nth-child(4) tr td:nth-child(1) { display: none; } #rightpreview > div:nth-child(4) tr td:nth-child(2)::before { content: "Preset "; } #rightpreview > div:nth-child(4) tr td:nth-child(2) { margin-bottom: 20px; } .trackbtn { background: none; } .trackbtn.start, .trackbtn.edit, .trackbtn.delete, .trackbtn.autopan { width: 150px; height: 30px; display: flex; align-items: center; justify-content: center; color: black; } .trackbtn.edit { background-color: yellow; margin-bottom: 7px; } .trackbtn.edit:before { content: 'EDIT'; } .trackbtn.delete { background-color: red; } .trackbtn.delete:before { content: 'DELETE'; } .trackbtn.start { background-color: green; height: 100px; } .trackbtn.start:before { font-size: 40px; content: 'GO'; } .trackbtn.autopan { background-color: green; opacity: 0.5; } .trackbtn.autopan:before { content: 'AUTO PAN'; } .trackbtn.autopan:hover { opacity: 1; } #autoPanButtonCell { margin-bottom: 2px; } #autoPanTimeCell { margin-bottom: 3px; } #autoPanTimeCell input { width: 50px; } .helpBtn { width: 150px; height: 30px; text-align: center; background-color: #ddd; opacity: 0.5; } .helpBtn:before { content: 'HELP'; } .helpBtn:hover { background-color: #ccc; opacity: 1; } .helpWindow { visibility: hidden; width: 100%; height: 100%; display: flex; position: absolute; top: 0; left: 0; z-index: 1; overflow-y: scroll; background-color: white; } .helpBtn:hover .helpWindow { visibility: visible; } .helpWindow ul { text-align: left; } `) function addGlobalStyle(css) { const head = mainDocument.getElementsByTagName('head')[0] if (!head) { return } const style = mainDocument.createElement('style') style.type = 'text/css' // Everything have to be !important to take effect. Sometimes this is undesirable so we undo the replace when !prevent-important is used. style.innerHTML = css.replace(/;/g, ' !important;').replace(/!prevent-important !important;/g, ';') head.appendChild(style) } }, 2000)