diff --git a/BMDevice.js b/BMDevice.js index b20e9d8..0b75000 100644 --- a/BMDevice.js +++ b/BMDevice.js @@ -17,272 +17,298 @@ // Generic Blackmagic Device class, use with HyperDecks. class BMDevice { - // Pretty name and network hostname (strings) - name; - hostname; - APIAddress; - - // Are we using HTTPS? - useHTTPS; - - // WebSocket items - ws; - availableProperties; - - // Active Flag - // Won't call updateUI if this is false - active = false; - - // JSON Object to store all data - propertyData = {}; - - // Reference to UI Updating callback function - // For BYOUI purposes (Bring-Your-Own-UI). If you're using this class for your own UI, - // set this function to point to your UI updater. - updateUI() {}; - - // ============= CONSTRUCTOR ================ - constructor(hostname, secure=false) { - // Set Security - this.useHTTPS = secure; - - // Set name properties - this.hostname = hostname; - this.APIAddress = (this.useHTTPS ? "https://" : "http://")+hostname+"/control/api/v1"; - this.name = this.hostname.replace(".local","").replaceAll("-"," "); - - // Initialize WebSocket - this.ws = new WebSocket((this.useHTTPS ? "wss://" : "ws://")+hostname+"/control/api/v1/event/websocket"); - - // Get a self object for accessing within callback fns - var self = this; - - // Set the onmessage behavior - this.ws.onmessage = (event) => { - // Parse the event's data as JSON - let eventData = JSON.parse(event.data); - - // Extract data we really care about - let messageData = eventData.data; - - // If it's a listProperties message, update the available properties array - if (messageData.action == "listProperties") { - self.availableProperties = messageData.properties; - } - - // If we get a response from the camera with property information, save it. - if (eventData.type == "response") { - Object.assign(this.propertyData, messageData.values); - } - - // If it's a propertyValueChanged event, update the camera object accordingly and show it on the web page. - if (messageData.action == "propertyValueChanged") { - this.propertyData[messageData.property] = messageData.value; - } - - if (this.active) { - // Update the UI - this.updateUI(); - } - - // Output info to console. - // console.log("WebSocket message received: ", eventData); + // Pretty name and network hostname (strings) + name; + hostname; + APIAddress; + + // Are we using HTTPS? + useHTTPS; + + // WebSocket items + ws; + availableProperties; + + // Active Flag + // Won't call updateUI if this is false + active = false; + + // JSON Object to store all data + propertyData = {}; + + // Reference to UI Updating callback function + // For BYOUI purposes (Bring-Your-Own-UI). If you're using this class for your own UI, + // set this function to point to your UI updater. + updateUI() {} + + // ============= CONSTRUCTOR ================ + constructor(hostname, secure = false) { + // Set Security + this.useHTTPS = secure; + + // Set name properties + this.hostname = hostname; + this.APIAddress = + (this.useHTTPS ? "https://" : "http://") + hostname + "/control/api/v1"; + this.name = this.hostname.replace(".local", "").replaceAll("-", " "); + + // Initialize WebSocket + this.ws = new WebSocket( + (this.useHTTPS ? "wss://" : "ws://") + + hostname + + "/control/api/v1/event/websocket" + ); + + // Get a self object for accessing within callback fns + var self = this; + let lastUIUpdate = 0; + const UI_UPDATE_INTERVAL = 50; + // Set the onmessage behavior + this.ws.onmessage = (event) => { + // Parse the event's data as JSON + let eventData = JSON.parse(event.data); + + // Extract data we really care about + let messageData = eventData.data; + + // If it's a listProperties message, update the available properties array + if (messageData.action === "listProperties") { + self.availableProperties = messageData.properties; + } + + // If we get a response from the camera with property information, save it. + if (eventData.type === "response") { + Object.assign(this.propertyData, messageData.values); + } + + // If it's a propertyValueChanged event, update the camera object accordingly + if (messageData.action === "propertyValueChanged") { + this.propertyData[messageData.property] = messageData.value; + } + + if (this.active) { + let now = Date.now(); + if (now - lastUIUpdate >= UI_UPDATE_INTERVAL) { + this.updateUI(); + lastUIUpdate = now; } + } - // Wait for the WebSocket to open - this.ws.onopen = (event) => { - // Once the WebSocket is open, - - // Ask it for all the properties - self.ws.send(JSON.stringify({type: "request", data: {action: "listProperties"}})); - - sleep(100).then(() => { - // Subscribe to all available events - this.availableProperties.forEach((str) => { - self.ws.send(JSON.stringify({type: "request", data: {action: "subscribe", properties: [str]}})); - }); - }); - } - } - - // Returns a JSON Object of data we got from the device - GETdata(endpoint) { - // Just call sendRequest - return sendRequest("GET", this.APIAddress+endpoint); - } - - // Send JSON Object data to the device - PUTdata(endpoint, data) { - // Just call sendRequest - return sendRequest("PUT", this.APIAddress+endpoint, data); - } - - // ================= SETTERS ================= - // Basically just wrappers for PUT requests to specific endpoints - - // If the optional parameter is set to false, it will stop recording - record(state = true) { - this.PUTdata("/transports/0/record",{recording: state}); - } - - toggleRecord() { - let recordState = this.propertyData['/transports/0/record'].recording; - - this.PUTdata("/transports/0/record",{recording: !recordState}); - } - - play() { - this.PUTdata("/transports/0/play"); - } - - stop() { - this.PUTdata("/transports/0/stop"); - } + // console.log("WebSocket message received: ", eventData); + }; - // Boolean parameter, true = forward, false = backwards - seek(direction) { - let clips = this.GETdata("/timelines/0")?.clips; - let playbackData = this.GETdata("/transports/0/playback"); - - let runningSum = 0; - let currentClipFound = false; - let currentClipIndex = 0; - let clipStartingTimecodes = []; - let i = 0; - - clips.forEach((clip) => { - if ((runningSum+clip.frameCount > playbackData.position) && !currentClipFound) { - currentClipIndex = i; - currentClipFound = true; - } - clipStartingTimecodes[i] = runningSum; - runningSum += clip.frameCount; - i++; + // Wait for the WebSocket to open + this.ws.onopen = (event) => { + // Once the WebSocket is open, + + // Ask it for all the properties + self.ws.send( + JSON.stringify({ type: "request", data: { action: "listProperties" } }) + ); + + sleep(100).then(() => { + // Subscribe to all available events + this.availableProperties.forEach((str) => { + self.ws.send( + JSON.stringify({ + type: "request", + data: { action: "subscribe", properties: [str] }, + }) + ); }); - - let newClipIndex = Math.min(Math.max(0,(direction ? currentClipIndex+1 : currentClipIndex-1)), clips.length-1); - - playbackData.position = clipStartingTimecodes[newClipIndex]; - - this.PUTdata("/transports/0/playback", playbackData); + }); + }; + } + + // Returns a JSON Object of data we got from the device + GETdata(endpoint) { + // Just call sendRequest + return sendRequest("GET", this.APIAddress + endpoint); + } + + // Send JSON Object data to the device + PUTdata(endpoint, data) { + // Just call sendRequest + return sendRequest("PUT", this.APIAddress + endpoint, data); + } + + // ================= SETTERS ================= + // Basically just wrappers for PUT requests to specific endpoints + + // If the optional parameter is set to false, it will stop recording + record(state = true) { + this.PUTdata("/transports/0/record", { recording: state }); + } + + toggleRecord() { + let recordState = this.propertyData["/transports/0/record"].recording; + + this.PUTdata("/transports/0/record", { recording: !recordState }); + } + + play() { + this.PUTdata("/transports/0/play"); + } + + stop() { + this.PUTdata("/transports/0/stop"); + } + + // Boolean parameter, true = forward, false = backwards + seek(direction) { + let clips = this.GETdata("/timelines/0")?.clips; + let playbackData = this.GETdata("/transports/0/playback"); + + let runningSum = 0; + let currentClipFound = false; + let currentClipIndex = 0; + let clipStartingTimecodes = []; + let i = 0; + + clips.forEach((clip) => { + if ( + runningSum + clip.frameCount > playbackData.position && + !currentClipFound + ) { + currentClipIndex = i; + currentClipFound = true; + } + clipStartingTimecodes[i] = runningSum; + runningSum += clip.frameCount; + i++; + }); + + let newClipIndex = Math.min( + Math.max(0, direction ? currentClipIndex + 1 : currentClipIndex - 1), + clips.length - 1 + ); + + playbackData.position = clipStartingTimecodes[newClipIndex]; + + this.PUTdata("/transports/0/playback", playbackData); + } + + // Sets Timeline / Clip Looping + // Argument can be either "None", "Loop", or "Loop Clip" + setLoopMode(modeString) { + let newStateObj = this.propertyData["/transports/0/playback"]; + + if (modeString === "None") { + newStateObj.loop = false; + newStateObj.singleClip = false; + } else if (modeString === "Loop") { + newStateObj.loop = true; + newStateObj.singleClip = false; + } else if (modeString === "Loop Clip") { + newStateObj.loop = true; + newStateObj.singleClip = true; } - // Sets Timeline / Clip Looping - // Argument can be either "None", "Loop", or "Loop Clip" - setLoopMode(modeString) { - let newStateObj = this.propertyData['/transports/0/playback']; - - if (modeString === "None") { - newStateObj.loop = false; - newStateObj.singleClip = false; - } else if (modeString === "Loop") { - newStateObj.loop = true; - newStateObj.singleClip = false; - } else if (modeString === "Loop Clip") { - newStateObj.loop = true; - newStateObj.singleClip = true; - } - - this.PUTdata("/transports/0/playback", newStateObj); - } + this.PUTdata("/transports/0/playback", newStateObj); + } } // Child Class Specifically for Cameras class BMCamera extends BMDevice { - // Child class constructor - // Just passing the hostname and security to the superclass's constructor - constructor(hostname, secure=false) { - super(hostname, secure); + // Child class constructor + // Just passing the hostname and security to the superclass's constructor + constructor(hostname, secure = false) { + super(hostname, secure); + } + + // Sets the white balance and tint based on the following preset: + // 0: Sunlight, 1: Tungsten, 2: Fluorescent, 3: Shade, 4: Cloudy + // Any other value will not affect the WB setting + setWhiteBalancePreset(presetIndex) { + let newWhiteBalance; + let newWhiteBalanceTint; + + switch (presetIndex) { + case 0: + // Sunlight + newWhiteBalance = 5600; + newWhiteBalanceTint = 10; + break; + case 1: + // Tungsten + newWhiteBalance = 3200; + newWhiteBalanceTint = 0; + break; + case 2: + // Fluorescent + newWhiteBalance = 4000; + newWhiteBalanceTint = 15; + break; + case 3: + // Shade + newWhiteBalance = 4500; + newWhiteBalanceTint = 15; + break; + case 4: + // Cloudy + newWhiteBalance = 6500; + newWhiteBalanceTint = 10; + break; + default: + // If any other value is set, don't change anything + newWhiteBalance = this.GETdata("/video/whiteBalance").whiteBalance; + newWhiteBalanceTint = this.GETdata( + "/video/whiteBalanceTint" + ).whiteBalanceTint; } - // Sets the white balance and tint based on the following preset: - // 0: Sunlight, 1: Tungsten, 2: Fluorescent, 3: Shade, 4: Cloudy - // Any other value will not affect the WB setting - setWhiteBalancePreset(presetIndex) { - let newWhiteBalance; - let newWhiteBalanceTint; - - switch (presetIndex) { - case 0: - // Sunlight - newWhiteBalance = 5600; - newWhiteBalanceTint = 10; - break; - case 1: - // Tungsten - newWhiteBalance = 3200; - newWhiteBalanceTint = 0; - break; - case 2: - // Fluorescent - newWhiteBalance = 4000; - newWhiteBalanceTint = 15; - break; - case 3: - // Shade - newWhiteBalance = 4500; - newWhiteBalanceTint = 15; - break; - case 4: - // Cloudy - newWhiteBalance = 6500; - newWhiteBalanceTint = 10; - break; - default: - // If any other value is set, don't change anything - newWhiteBalance = this.GETdata("/video/whiteBalance").whiteBalance; - newWhiteBalanceTint = this.GETdata("/video/whiteBalanceTint").whiteBalanceTint; - } - - this.PUTdata("/video/whiteBalance",{whiteBalance: newWhiteBalance}); - this.PUTdata("/video/whiteBalanceTint",{whiteBalanceTint: newWhiteBalanceTint}); - } + this.PUTdata("/video/whiteBalance", { whiteBalance: newWhiteBalance }); + this.PUTdata("/video/whiteBalanceTint", { + whiteBalanceTint: newWhiteBalanceTint, + }); + } - doAutoFocus() { - this.PUTdata("/lens/focus/doAutoFocus"); - } + doAutoFocus() { + this.PUTdata("/lens/focus/doAutoFocus"); + } - doAutoWhitebalance() { - this.PUTdata("/video/whiteBalance/doAuto"); - } + doAutoWhitebalance() { + this.PUTdata("/video/whiteBalance/doAuto"); + } } /* Helper Functions */ // Send request with other method type function sendRequest(method, url, data) { - // Instantiate the XMLHttpRequest object - let xhr = new XMLHttpRequest(); - - // Create an object to store and return the response - let responseObject = {}; - - // Define the onload function - xhr.onload = function() { - if (this.status < 300) { // If the operation is successful - if (this.responseText) - responseObject = JSON.parse(this.responseText); // Give the data to the responseObject - responseObject.status = this.status; // Also pass along the status code for error handling - } else { // If there has been an error - responseObject = this; // Give the XMLHttpRequest data to the responseObject - console.error("Error ", this.status, ": ", this.statusText); // Log the error in the console - } - }; + // Instantiate the XMLHttpRequest object + let xhr = new XMLHttpRequest(); + + // Create an object to store and return the response + let responseObject = {}; + + // Define the onload function + xhr.onload = function () { + if (this.status < 300) { + // If the operation is successful + if (this.responseText) responseObject = JSON.parse(this.responseText); // Give the data to the responseObject + responseObject.status = this.status; // Also pass along the status code for error handling + } else { + // If there has been an error + responseObject = this; // Give the XMLHttpRequest data to the responseObject + console.error("Error ", this.status, ": ", this.statusText); // Log the error in the console + } + }; - // Open the connection - // The "false" here specifies that we want to wait for the response to come back before returning from xhr.send() - xhr.open(method, url, false); + // Open the connection + // The "false" here specifies that we want to wait for the response to come back before returning from xhr.send() + xhr.open(method, url, false); - // Send the request with data - xhr.send(JSON.stringify(data)); + // Send the request with data + xhr.send(JSON.stringify(data)); - // Return response data - return responseObject; + // Return response data + return responseObject; } function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } /* (c) Dylan Speiser 2024 - github.com/DylanSpeiser */ \ No newline at end of file + github.com/DylanSpeiser */ diff --git a/index.html b/index.html index 92b0fac..4df19cd 100644 --- a/index.html +++ b/index.html @@ -2,348 +2,421 @@ - - - Camera Control WebUI for Blackmagic Cameras - - - - - - - - - - + + + Camera Control WebUI for Blackmagic Cameras + + + - + + + - -
-

Camera Control WebUI for Blackmagic Cameras

-
+ + + + - -
- CAM1 - | - CAM2 - | - CAM3 - | - CAM4 - | - CAM5 - | - CAM6 - | - CAM7 - | - CAM8 -
+ - -
-
-
-

CAM1

-
+ +
+

Camera Control WebUI for Blackmagic Cameras+

+
+ + +
+ CAM1 + | + CAM2 + | + CAM3 + | + CAM4 + | + CAM5 + | + CAM6 + | + CAM7 + | + CAM8 +
+ + +
+
+
+

CAM1

+
-
- - Lift -
- -
- 0.00 - 0.00 - 0.00 - 0.00 -
- + Lift +
+ + +
+ 0.00 + 0.00 + 0.00 + 0.00
+ +
- Gamma -
- -
- 0.00 - 0.00 - 0.00 - 0.00 -
- + Gamma +
+ + +
+ 0.00 + 0.00 + 0.00 + 0.00
+ +
- Gain -
- -
- 0.00 - 0.00 - 0.00 - 0.00 -
- + Gain +
+ + +
+ 0.00 + 0.00 + 0.00 + 0.00
- + +
- Offset -
- -
- 0.00 - 0.00 - 0.00 - 0.00 -
- + + Offset +
+ + +
+ 0.00 + 0.00 + 0.00 + 0.00
+
+
-
-
- FILTER -
- - 0 - -
+
+
+ FILTER +
+ + 0 +
-
- GAIN -
- - +0db - -
+
+
+ GAIN +
+ + +0db +
-
- SHUTTER -
- - 1/50 - -
+
+
+ SHUTTER +
+ + 1/50 +
-
- BALANCE -
- - 5600K - -
-
- - 0 - -
+
+
+ BALANCE +
+ + 5600K +
-
- +
+ + 0 +
+
+ +
+
-
-
- FOCUS - - -
-
- IRIS - - X.X -
-
- ZOOM - - XXmm -
+
+
+ FOCUS + + +
+
+ IRIS + + X.X +
+
+ ZOOM + + XXmm
+
+ +
+ +
+
+

CAMERA NAME

+
+ + + + + + + + +
+
+ + + + + + + + +
+

TIMECODE

-
-
-

CAMERA NAME

-
- CODEC - RESOLUTION - FPS -
-
- - - - - - - - -
-

TIMECODE

-
+
+
+

Connection

+ + + + + + + + + + + + +
Hostname + + + + Use HTTPS + +
Send API Call + + + + -
-
-

Connection

- - - - - - - - - - - - -
Hostname - - - - Use HTTPS - -
Send API Call - - - - + + - - - - -
-

Send manual API requests using the above controls. See documentation for details.

-
-
- -
-

Presets

- - - - - - -
Preset Select - - - -
-
- -
-

Exposure

- - - - - - - - - - - - - -
ISO
AE Mode - -
AE Type - -
- -
- -
-

Contrast

- - - - - - - - - - - - -
Pivot - 0 - - -
Adjust - 0 -
-
- -
-

Color

- - - - - - - - - - - - - - - - - -
Hue - 0 - - -
Saturation - 0 -
Luma Contribution - 0 -
-
-
+ +
+

Send manual API requests using the above controls. See documentation for details.

+
+
+ +
+

Presets

+ + + + + + +
Preset Select + + + +
+
+ +
+

Exposure

+ + + + + + + + + + + + + +
ISO
AE Mode + +
AE Type + +
+ +
+ +
+

Contrast

+ + + + + + + + + + + + +
Pivot + + 0 + + +
Adjust + + 0 +
+
+ +
+

Color

+ + + + + + + + + + + + + + + + + +
Hue + + 0 + + +
Saturation + + 0 +
Luma Contribution + + 0 +
+
-
- -
-
- (v 1.4.2) - -
- +
+ + +
+
+ (v 1.4.2) +
- - \ No newline at end of file + +
+ + + diff --git a/style.css b/style.css index e71a457..6d12e70 100644 --- a/style.css +++ b/style.css @@ -342,6 +342,62 @@ h2 { #cameraControlColorCorrectionNumbersContainer span { margin: 0px 0.5em; text-decoration: underline 3px; + display: inline-block; + min-width: 4.4ch; + text-align: center; + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum"; + white-space: nowrap; + cursor: text; + user-select: text; +} + +#cameraControlColorCorrectionNumbersContainer span:hover { + background: rgba(255, 255, 255, 0.04); + border-radius: 0.2em; +} + +.ccNumberDragActive { + cursor: ew-resize !important; + background: rgba(255, 255, 255, 0.08); + border-radius: 0.2em; +} + +.ccNumberDragging { + cursor: ew-resize !important; +} + +.ccNumberDragging * { + cursor: ew-resize !important; +} + +.ccWheel { + width: 3.6em; + height: 3.6em; + margin: 0 0.25em; + border-radius: 50%; + background: radial-gradient(circle at 50% 50%, #3a3a3a 0%, #202020 52%, #101010 100%); + border: 0.25em solid #3f3f3f; + box-shadow: + inset 0 0 0 0.15em #121212, + 0 0 0 0.08em #0a0a0a; + cursor: grab; + touch-action: none; + flex-shrink: 0; + user-select: none; +} + +.ccWheel:active { + border-color: #666; + cursor: grabbing; +} + +.draggingWheel { + cursor: none !important; +} + +.draggingWheel * { + cursor: none !important; } /* Exposure Section */ @@ -467,4 +523,4 @@ table, td { td, tr { align-items: center; -} \ No newline at end of file +} diff --git a/web-ui.js b/web-ui.js index 83b1df5..ee6dad3 100644 --- a/web-ui.js +++ b/web-ui.js @@ -4,652 +4,1367 @@ github.com/DylanSpeiser */ - /* Global variables */ -var cameras = []; // Array to store all of the camera objects -var ci = 0; // Index into this array for the currently selected camera. +var cameras = []; // Array to store all of the camera objects +var ci = 0; // Index into this array for the currently selected camera. // cameras[ci] is used to reference the currently selected camera object -var WBMode = 0; // 0: balance, 1: tint +var WBMode = 0; // 0: balance, 1: tint var defaultControlsHTML; var unsavedChanges = []; +var ccWheelState = {}; +var ccNumberState = {}; // Set everything up function bodyOnLoad() { - defaultControlsHTML = document.getElementById("allCamerasContainer").innerHTML; - // prefill camera hostname (or IP address) - document.getElementById("hostnameInput").value = localStorage.getItem("camerahostname_"+ci.toString()); - if ( localStorage.getItem("camerasecurity_"+ci.toString()) === 'true' ) { - document.getElementById("secureCheckbox").checked = true - } + defaultControlsHTML = document.getElementById( + "allCamerasContainer" + ).innerHTML; + // prefill camera hostname (or IP address) + document.getElementById("hostnameInput").value = localStorage.getItem( + "camerahostname_" + ci.toString() + ); + if (localStorage.getItem("camerasecurity_" + ci.toString()) === "true") { + document.getElementById("secureCheckbox").checked = true; + } + + setupColorWheels(); + setupCCNumberFields(); } - -// Checks the hostname, if it replies successfully then a new BMCamera object -// is made and gets put in the array at ind -function initCamera() { - // Get hostname from Hostname text field - let hostname = document.getElementById("hostnameInput").value; - let security = document.getElementById("secureCheckbox").checked; - - try { - // Check if the hostname is valid - let response = sendRequest("GET", (security ? "https://" : "http://")+hostname+"/control/api/v1/system",""); - - if (response.status < 300) { - // Success, make a new camera, get all relevant info, and populate the UI - cameras[ci] = new BMCamera(hostname, security); - // Save camera hostname and security status in local storage - localStorage.setItem("camerahostname_"+ci, hostname) - localStorage.setItem("camerasecurity_"+ci, security) - cameras[ci].updateUI = updateUIAll; - - cameras[ci].active = true; - - document.getElementById("connectionErrorSpan").innerHTML = "Connected."; - document.getElementById("connectionErrorSpan").setAttribute("style","color: #6e6e6e;"); - - } else { - // Something has gone wrong, tell the user - document.getElementById("connectionErrorSpan").innerHTML = response.statusText; +function setupCCNumberFields() { + document.querySelectorAll('[data-cc-row]').forEach((el) => { + const row = parseInt(el.dataset.ccRow, 10); + const key = `${row}-${el.className}`; + const cfg = { + row, + step: row === 2 ? 0.015 : 0.005, + decimals: 2, + min: row === 2 && el.classList.contains("CClumaLabel") ? 0 : null, + max: row === 2 && el.classList.contains("CClumaLabel") ? 3 : null, + }; + + ccNumberState[key] = { + dragging: false, + startX: 0, + startValue: 0, + moved: false, + cfg, + }; + + el.addEventListener("mousedown", (ev) => { + ev.preventDefault(); + ccNumberState[key].startX = ev.clientX; + ccNumberState[key].startValue = parseCCNumberText(el.innerText); + ccNumberState[key].moved = false; + ccNumberState[key].dragging = true; + el.classList.add("ccNumberDragActive"); + document.body.classList.add("ccNumberDragging"); + + const moveHandler = (moveEv) => { + if (!ccNumberState[key].dragging) return; + moveEv.preventDefault(); + + const deltaX = moveEv.clientX - ccNumberState[key].startX; + if (!ccNumberState[key].moved && Math.abs(deltaX) < 4) return; + ccNumberState[key].moved = true; + + const newValue = ccNumberState[key].startValue + deltaX * cfg.step; + const clamped = clampNumberByField(cfg, newValue); + setCCNumberValue(el, clamped, cfg.decimals); + unsavedChanges.push(`CC${row}`); + }; + + const upHandler = () => { + ccNumberState[key].dragging = false; + el.classList.remove("ccNumberDragActive"); + document.body.classList.remove("ccNumberDragging"); + window.removeEventListener("mousemove", moveHandler); + window.removeEventListener("mouseup", upHandler); + + if (!ccNumberState[key].moved) { + el.focus(); + placeCaretAtEnd(el); + return; } - } catch (error) { - // Something has gone wrong, tell the user - document.getElementById("connectionErrorSpan").title = error; - document.getElementById("connectionErrorSpan").innerHTML = "Error "+error.code+": "+error.name+" (Your hostname is probably incorrect, hover for more details)"; - } - unsavedChanges = unsavedChanges.filter((e) => {return e !== "Hostname"}); + commitCCNumberField(el); + }; + + window.addEventListener("mousemove", moveHandler, { passive: false }); + window.addEventListener("mouseup", upHandler, { once: true }); + }); + + el.addEventListener("keydown", (ev) => { + if (ev.key === "Enter") { + ev.preventDefault(); + el.blur(); + commitCCNumberField(el); + } else { + unsavedChanges.push(`CC${row}`); + } + }); + + el.addEventListener("blur", () => { + if (!ccNumberState[key].dragging) commitCCNumberField(el); + }); + + el.addEventListener("dragstart", (ev) => ev.preventDefault()); + }); } -// =============================== UI Updater ================================== -// ============================================================================= +function normalizeCCNumberText(text) { + return String(text || "").replace(/[^\d.\-+]/g, ""); +} -function updateUIAll() { - // ========== Camera Name ========== +function parseCCNumberText(text) { + const parsed = parseFloat(normalizeCCNumberText(text)); + return Number.isFinite(parsed) ? parsed : 0; +} - document.getElementById("cameraName").innerHTML = cameras[ci].name; +function setCCNumberValue(el, value, decimals) { + el.innerText = value.toFixed(decimals); +} - // ========== Hostname ========== +function placeCaretAtEnd(el) { + const range = document.createRange(); + range.selectNodeContents(el); + range.collapse(false); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); +} - if (!unsavedChanges.includes("Hostname")) { - document.getElementById("hostnameInput").value = cameras[ci].hostname; - } +function clampNumberByField(cfg, value) { + if (cfg.min !== null) value = Math.max(cfg.min, value); + if (cfg.max !== null) value = Math.min(cfg.max, value); + return value; +} - // ========== Format ========== +function commitCCNumberField(el) { + const row = parseInt(el.dataset.ccRow, 10); + const value = parseCCNumberText(el.innerText); + setCCNumberValue(el, value, 2); + setCCFromUI(row); +} - document.getElementById("formatCodec").innerHTML = cameras[ci].propertyData['/system/format']?.codec.toUpperCase().replace(":"," ").replace("_",":"); - - let resObj = cameras[ci].propertyData['/system/format']?.recordResolution; - document.getElementById("formatResolution").innerHTML = resObj?.width + "x" + resObj?.height; - document.getElementById("formatFPS").innerHTML = cameras[ci].propertyData['/system/format']?.frameRate+" fps"; +function setupColorWheels() { + document.querySelectorAll(".ccWheel").forEach((wheel) => { + const ccIndex = parseInt(wheel.dataset.cc, 10); + ccWheelState[ccIndex] = { + x: 0, + y: 0, + dragging: false, + locked: false, + dragBaseX: 0, + dragBaseY: 0, + dragStartX: 0, + dragStartY: 0, + lastLocalApplyAt: 0, + lastSent: { r: null, g: null, b: null, luma: null }, + lastApplied: { x: 0, y: 0, radius: 0 }, + }; + + const updateFromPointer = (clientX, clientY) => { + const rect = wheel.getBoundingClientRect(); + const rawDx = (clientX - ccWheelState[ccIndex].dragStartX) / (rect.width / 2); + const rawDy = (clientY - ccWheelState[ccIndex].dragStartY) / (rect.height / 2); + const sensitivity = 0.18; + const dx = clamp(ccWheelState[ccIndex].dragBaseX + rawDx * sensitivity, -1, 1); + const dy = clamp(ccWheelState[ccIndex].dragBaseY + rawDy * sensitivity, -1, 1); + const radius = clamp(Math.sqrt(dx * dx + dy * dy), 0, 1); + ccWheelState[ccIndex].x = dx; + ccWheelState[ccIndex].y = dy; + applyCCWheel(ccIndex, dx, dy, radius); + }; + + wheel.addEventListener("mousedown", (ev) => { + ev.preventDefault(); + ccWheelState[ccIndex].dragging = true; + ccWheelState[ccIndex].locked = true; + document.body.classList.add("draggingWheel"); + ccWheelState[ccIndex].dragStartX = ev.clientX; + ccWheelState[ccIndex].dragStartY = ev.clientY; + ccWheelState[ccIndex].dragBaseX = ccWheelState[ccIndex].x || 0; + ccWheelState[ccIndex].dragBaseY = ccWheelState[ccIndex].y || 0; + let moved = false; + + const moveHandler = (moveEv) => { + if (!ccWheelState[ccIndex].dragging) return; + moveEv.preventDefault(); + if (!moved) { + const deltaX = Math.abs(moveEv.clientX - ccWheelState[ccIndex].dragStartX); + const deltaY = Math.abs(moveEv.clientY - ccWheelState[ccIndex].dragStartY); + if (deltaX < 2 && deltaY < 2) return; + moved = true; + } + updateFromPointer(moveEv.clientX, moveEv.clientY); + }; + + const upHandler = () => { + ccWheelState[ccIndex].dragging = false; + ccWheelState[ccIndex].locked = false; + document.body.classList.remove("draggingWheel"); + window.removeEventListener("mousemove", moveHandler); + window.removeEventListener("mouseup", upHandler); + }; + + window.addEventListener("mousemove", moveHandler, { passive: false }); + window.addEventListener("mouseup", upHandler, { once: true }); + }); + + wheel.addEventListener("dragstart", (ev) => ev.preventDefault()); + }); +} - // ========== Recording State ========== +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} - if (cameras[ci].propertyData['/transports/0/record']?.recording) { - document.getElementById("cameraControlHeadContainer").classList.add("liveCam"); - document.getElementById("cameraControlExpandedHeadContainer").classList.add("liveCam"); - } else { - document.getElementById("cameraControlHeadContainer").classList.remove("liveCam"); - document.getElementById("cameraControlExpandedHeadContainer").classList.remove("liveCam"); - } +function hslToRgb(h, s, l) { + const c = (1 - Math.abs(2 * l - 1)) * s; + const hp = h / 60; + const x = c * (1 - Math.abs((hp % 2) - 1)); + let r = 0, g = 0, b = 0; + if (hp >= 0 && hp < 1) { + r = c; g = x; + } else if (hp < 2) { + r = x; g = c; + } else if (hp < 3) { + g = c; b = x; + } else if (hp < 4) { + g = x; b = c; + } else if (hp < 5) { + r = x; b = c; + } else { + r = c; b = x; + } + const m = l - c / 2; + return { r: r + m, g: g + m, b: b + m }; +} - // ========== Playback Loop State ========== - let loopState = cameras[ci].propertyData['/transports/0/playback']?.loop; - let singleClipState = cameras[ci].propertyData['/transports/0/playback']?.singleClip; +function applyCCWheel(which, dx, dy, radius) { + const angle = (Math.atan2(-dy, dx) * 180) / Math.PI; + const hue = (angle + 360) % 360; + const sat = clamp(Math.max(0, radius - 0.08) / 0.92, 0, 1); + const rgb = hslToRgb(hue, sat, 0.5); + const neutral = which === 2 ? 1 : 0; + const scale = which === 2 ? 1.1 : 0.7; + const ccobject = { + red: clamp(neutral + (rgb.r - 0.5) * scale, which === 2 ? 0 : -1, which === 2 ? 2 : 1), + green: clamp(neutral + (rgb.g - 0.5) * scale, which === 2 ? 0 : -1, which === 2 ? 2 : 1), + blue: clamp(neutral + (rgb.b - 0.5) * scale, which === 2 ? 0 : -1, which === 2 ? 2 : 1), + luma: parseFloat(document.getElementsByClassName("CClumaLabel")[which].innerHTML), + }; + + if (which === 2) { + ccobject.luma = 1.0; + } + + if (which === 0) { + cameras[ci].PUTdata("/colorCorrection/lift", ccobject); + } else if (which === 1) { + cameras[ci].PUTdata("/colorCorrection/gamma", ccobject); + } else if (which === 2) { + cameras[ci].PUTdata("/colorCorrection/gain", ccobject); + } else if (which === 3) { + cameras[ci].PUTdata("/colorCorrection/offset", ccobject); + } + + ccWheelState[which] = ccWheelState[which] || { x: 0, y: 0, dragging: false, locked: false, lastApplied: { x: 0, y: 0, radius: 0 } }; + ccWheelState[which].x = dx; + ccWheelState[which].y = dy; + ccWheelState[which].lastApplied = { x: dx, y: dy, radius: radius }; + ccWheelState[which].lastLocalApplyAt = Date.now(); + ccWheelState[which].lastSent = { + r: ccobject.red, + g: ccobject.green, + b: ccobject.blue, + luma: ccobject.luma, + }; + + unsavedChanges = unsavedChanges.filter((e) => !e.includes("CC" + which)); + drawCCWheel(which, dx, dy, radius); +} - let loopButton = document.getElementById("loopButton"); - let singleClipButton = document.getElementById("singleClipButton"); +function drawCCWheel(which, dx, dy, radius) { + const wheel = document.querySelector(`.ccWheel[data-cc="${which}"]`); + if (!wheel) return; + const ctx = wheel.getContext("2d"); + const size = wheel.width; + const center = size / 2; + ctx.clearRect(0, 0, size, size); + + const gradient = ctx.createRadialGradient(center, center, 6, center, center, center - 5); + gradient.addColorStop(0, "#555"); + gradient.addColorStop(1, "#151515"); + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(center, center, center - 5, 0, Math.PI * 2); + ctx.fill(); + + ctx.lineWidth = 4; + ctx.strokeStyle = "#666"; + ctx.beginPath(); + ctx.arc(center, center, center - 7, 0, Math.PI * 2); + ctx.stroke(); + + const hue = ((Math.atan2(-dy, dx) * 180) / Math.PI + 360) % 360; + const color = hslToRgb(hue, radius, 0.5); + ctx.fillStyle = `rgba(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}, 0.95)`; + ctx.beginPath(); + ctx.arc(center + dx * (center - 16), center + dy * (center - 16), 10, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = "#eaeaea"; + ctx.lineWidth = 2; + ctx.stroke(); + + ctx.strokeStyle = "rgba(255,255,255,0.2)"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.arc(center, center, center - 16, 0, Math.PI * 2); + ctx.stroke(); + + ctx.fillStyle = "rgba(255,255,255,0.75)"; + ctx.beginPath(); + ctx.arc(center, center, 4, 0, Math.PI * 2); + ctx.fill(); +} - if (loopState) { - loopButton.classList.add("activated"); - } else { - loopButton.classList.remove("activated"); - } +function syncCCWheel(which) { + if (ccWheelState[which]?.locked) return; + if (ccWheelState[which]?.dragging) return; + const state = ccWheelState[which]; + if (Date.now() - (state?.lastLocalApplyAt || 0) < 700) return; + const rEl = document.getElementsByClassName("CCredLabel")[which]; + const gEl = document.getElementsByClassName("CCgreenLabel")[which]; + const bEl = document.getElementsByClassName("CCblueLabel")[which]; + if (!rEl || !gEl || !bEl) return; + + const r = parseFloat(rEl.innerHTML); + const g = parseFloat(gEl.innerHTML); + const b = parseFloat(bEl.innerHTML); + const lastSent = state?.lastSent || {}; + const nearlySame = + lastSent.r !== null && + Math.abs(r - lastSent.r) < 0.03 && + Math.abs(g - lastSent.g) < 0.03 && + Math.abs(b - lastSent.b) < 0.03; + if (nearlySame) return; + const neutral = which === 2 ? 1 : 0; + const scale = which === 2 ? 2 : 2; + const rr = (r - neutral) / scale + 0.5; + const gg = (g - neutral) / scale + 0.5; + const bb = (b - neutral) / scale + 0.5; + const max = Math.max(rr, gg, bb); + const min = Math.min(rr, gg, bb); + const sat = max === 0 ? 0 : (max - min) / max; + + let hue = 0; + if (max !== min) { + if (max === rr) hue = (60 * ((gg - bb) / (max - min)) + 360) % 360; + else if (max === gg) hue = 60 * ((bb - rr) / (max - min)) + 120; + else hue = 60 * ((rr - gg) / (max - min)) + 240; + } + + const radius = clamp(sat, 0, 1); + const rad = (hue * Math.PI) / 180; + const dx = Math.cos(rad) * radius; + const dy = -Math.sin(rad) * radius; + ccWheelState[which] = ccWheelState[which] || { + x: 0, + y: 0, + dragging: false, + locked: false, + lastSent: { r: null, g: null, b: null, luma: null }, + lastApplied: { x: 0, y: 0, radius: 0 }, + }; + ccWheelState[which].x = dx; + ccWheelState[which].y = dy; + ccWheelState[which].lastApplied = { x: dx, y: dy, radius: radius }; + drawCCWheel(which, dx, dy, radius); +} - if (singleClipState) { - singleClipButton.classList.add("activated"); +// Checks the hostname, if it replies successfully then a new BMCamera object +// is made and gets put in the array at ind +function initCamera() { + // Get hostname from Hostname text field + let hostname = document.getElementById("hostnameInput").value; + let security = document.getElementById("secureCheckbox").checked; + + try { + // Check if the hostname is valid + let response = sendRequest( + "GET", + (security ? "https://" : "http://") + hostname + "/control/api/v1/system", + "" + ); + + if (response.status < 300) { + // Success, make a new camera, get all relevant info, and populate the UI + cameras[ci] = new BMCamera(hostname, security); + // Save camera hostname and security status in local storage + localStorage.setItem("camerahostname_" + ci, hostname); + localStorage.setItem("camerasecurity_" + ci, security); + cameras[ci].updateUI = updateUIAll; + + cameras[ci].active = true; + let supportedFormats = sendRequest( + "GET", + (security ? "https://" : "http://") + + hostname + + "/control/api/v1/system/supportedFormats" + ); + + cameras[ci].propertyData[`/system/supportedFormats`] = supportedFormats; + + document.getElementById("connectionErrorSpan").innerHTML = "Connected."; + document + .getElementById("connectionErrorSpan") + .setAttribute("style", "color: #6e6e6e;"); } else { - singleClipButton.classList.remove("activated"); + // Something has gone wrong, tell the user + document.getElementById("connectionErrorSpan").innerHTML = + response.statusText; } + } catch (error) { + // Something has gone wrong, tell the user + document.getElementById("connectionErrorSpan").title = error; + document.getElementById("connectionErrorSpan").innerHTML = + "Error " + + error.code + + ": " + + error.name + + " (Your hostname is probably incorrect, hover for more details)"; + } + + unsavedChanges = unsavedChanges.filter((e) => { + return e !== "Hostname"; + }); +} - // ========== Timecode ========== +// =============================== UI Updater ================================== +// ============================================================================= - document.getElementById("timecodeLabel").innerHTML = parseTimecode(cameras[ci].propertyData['/transports/0/timecode']?.timecode); +function updateUIAll() { + // ========== Camera Name ========== - // ========== Presets Dropdown ========== + document.getElementById("cameraName").innerHTML = cameras[ci].name; - if (!unsavedChanges.includes("presets")) { - - var presetsList = document.getElementById("presetsDropDown"); + // ========== Hostname ========== - presetsList.innerHTML = ""; + if (!unsavedChanges.includes("Hostname")) { + document.getElementById("hostnameInput").value = cameras[ci].hostname; + } - cameras[ci].propertyData['/presets']?.presets.forEach((presetItem) => { - let presetName = presetItem.split('.', 1); + // ========== Format ========== + // Селекты + const codecSelect = document.getElementById("formatCodecSelect"); + const resSelect = document.getElementById("formatResSelect"); + const fpsSelect = document.getElementById("formatFPSSelect"); - let textNode = document.createTextNode(presetName); - let optionNode = document.createElement("option"); - optionNode.setAttribute("name", "presetOption"+presetName); - optionNode.appendChild(textNode); - document.getElementById("presetsDropDown").appendChild(optionNode); - }); + // Объект камеры + const camera = cameras[ci]; // твой объект камеры - // ========== Active Preset ========== + // Функция для обновления списка кодеков + function updateCodecs() { + const codecs = camera.propertyData[ + "/system/supportedFormats" + ].supportedFormats + .map((f) => f.codecs) + .flat() + .map((c) => c); - var presetsList = document.getElementById("presetsDropDown"); + const uniqueCodecs = [...new Set(codecs)]; - presetsList.childNodes.forEach((child) => { - if (child.nodeName == 'OPTION' && (child.value+".cset") == cameras[ci].propertyData['/presets/active']?.preset) { - child.selected=true - } else { - child.selected=false - } - }) + codecSelect.innerHTML = uniqueCodecs + .map((c) => ``) + .join(""); - } + const currentCodec = camera.propertyData["/system/format"]?.codec; - // ========== Iris ========== + codecSelect.value = currentCodec; - document.getElementById("irisRange").value = cameras[ci].propertyData['/lens/iris']?.normalised; - document.getElementById("apertureStopsLabel").innerHTML = cameras[ci].propertyData['/lens/iris']?.apertureStop.toFixed(1); + updateResolutions(); + } - // ========== Zoom ========== + function updateResolutions() { + const selectedCodec = codecSelect.value; + const formats = + camera.propertyData["/system/supportedFormats"].supportedFormats; - document.getElementById("zoomRange").value = cameras[ci].propertyData['/lens/zoom']?.normalised; - document.getElementById("zoomMMLabel").innerHTML = cameras[ci].propertyData['/lens/zoom']?.focalLength +"mm"; + const availableRes = formats + .filter((f) => f.codecs.includes(selectedCodec)) + .map((f) => `${f.recordResolution.width}x${f.recordResolution.height}`); - // ========== Focus ========== + const uniqueRes = [...new Set(availableRes)]; - document.getElementById("focusRange").value = cameras[ci].propertyData['/lens/focus']?.normalised; + resSelect.innerHTML = uniqueRes + .map((r) => ``) + .join(""); - // ========== ISO ========== - if (!unsavedChanges.includes("ISO")) { - if (cameras[ci].propertyData['/video/iso']) - document.getElementById("ISOInput").value = cameras[ci].propertyData['/video/iso']?.iso; - } + const currentRes = camera.propertyData["/system/format"]?.recordResolution; + resSelect.value = `${currentRes?.width}x${currentRes?.height}`; - // ========== GAIN ========== + updateFPS(); + } - if (!unsavedChanges.includes("Gain")) { - let gainString = ""; - let gainInt = cameras[ci].propertyData['/video/gain']?.gain + function updateFPS() { + const selectedCodec = codecSelect.value; + const selectedRes = resSelect.value.split("x").map(Number); - if (gainInt >= 0) { - gainString = "+"+gainInt+"db" - } else { - gainString = gainInt+"db" - } + const formats = + camera.propertyData["/system/supportedFormats"].supportedFormats; + const matchingFormat = formats.find( + (f) => + f.codecs.includes(selectedCodec) && + f.recordResolution.width === selectedRes[0] && + f.recordResolution.height === selectedRes[1] + ); - document.getElementById("gainSpan").innerHTML = gainString; + const fpsOptions = matchingFormat?.frameRates || []; + fpsSelect.innerHTML = fpsOptions + .map((fps) => ``) + .join(""); + + const currentFPS = camera.propertyData["/system/format"]?.frameRate; + if (fpsOptions.includes(currentFPS)) { + fpsSelect.value = `${currentFPS} fps`; } + } + + codecSelect.addEventListener("change", updateResolutions); + resSelect.addEventListener("change", updateFPS); + + updateCodecs(); + + // ========== Recording State ========== + + if (cameras[ci].propertyData["/transports/0/record"]?.recording) { + document + .getElementById("cameraControlHeadContainer") + .classList.add("liveCam"); + document + .getElementById("cameraControlExpandedHeadContainer") + .classList.add("liveCam"); + } else { + document + .getElementById("cameraControlHeadContainer") + .classList.remove("liveCam"); + document + .getElementById("cameraControlExpandedHeadContainer") + .classList.remove("liveCam"); + } + + // ========== Playback Loop State ========== + let loopState = cameras[ci].propertyData["/transports/0/playback"]?.loop; + let singleClipState = + cameras[ci].propertyData["/transports/0/playback"]?.singleClip; + + let loopButton = document.getElementById("loopButton"); + let singleClipButton = document.getElementById("singleClipButton"); + + if (loopState) { + loopButton.classList.add("activated"); + } else { + loopButton.classList.remove("activated"); + } + + if (singleClipState) { + singleClipButton.classList.add("activated"); + } else { + singleClipButton.classList.remove("activated"); + } + + // ========== Timecode ========== + + document.getElementById("timecodeLabel").innerHTML = parseTimecode( + cameras[ci].propertyData["/transports/0/timecode"]?.timecode + ); + + // ========== Presets Dropdown ========== + + if (!unsavedChanges.includes("presets")) { + var presetsList = document.getElementById("presetsDropDown"); + + presetsList.innerHTML = ""; + + cameras[ci].propertyData["/presets"]?.presets.forEach((presetItem) => { + let presetName = presetItem.split(".", 1); + + let textNode = document.createTextNode(presetName); + let optionNode = document.createElement("option"); + optionNode.setAttribute("name", "presetOption" + presetName); + optionNode.appendChild(textNode); + document.getElementById("presetsDropDown").appendChild(optionNode); + }); - // ========== WHITE BALANCE =========== + // ========== Active Preset ========== - if (!unsavedChanges.includes("WB")) { - document.getElementById("whiteBalanceSpan").innerHTML = cameras[ci].propertyData['/video/whiteBalance']?.whiteBalance+"K"; - } - - if (!unsavedChanges.includes("WBT")) { - document.getElementById("whiteBalanceTintSpan").innerHTML = cameras[ci].propertyData['/video/whiteBalanceTint']?.whiteBalanceTint; - } + var presetsList = document.getElementById("presetsDropDown"); - // =========== ND ============= + presetsList.childNodes.forEach((child) => { + if ( + child.nodeName == "OPTION" && + child.value + ".cset" == + cameras[ci].propertyData["/presets/active"]?.preset + ) { + child.selected = true; + } else { + child.selected = false; + } + }); + } - if (!unsavedChanges.includes("ND")) { - if (cameras[ci].propertyData['/video/ndFilter']) { - document.getElementById("ndFilterSpan").innerHTML = cameras[ci].propertyData['/video/ndFilter']?.stop; - } else { - document.getElementById("ndFilterSpan").innerHTML = 0; - document.getElementById("ndFilterSpan").disabled = true; - } - } + // ========== Iris ========== - // ============ Shutter ===================== - - if (!unsavedChanges.includes("Shutter")) { - let shutterString = "SS" - let shutterObj = cameras[ci].propertyData['/video/shutter']; - - if (shutterObj?.shutterSpeed) { - shutterString = "1/"+shutterObj.shutterSpeed - } else if (shutterObj?.shutterAngle) { - var shangleString = (shutterObj.shutterAngle / 100).toFixed(1).toString() - if (shangleString.indexOf(".0") > 0) { - shutterString = parseFloat(shangleString).toFixed(0)+"°"; - } else { - shutterString = shangleString+"°"; - } - } + document.getElementById("irisRange").value = + cameras[ci].propertyData["/lens/iris"]?.normalised; + document.getElementById("apertureStopsLabel").innerHTML = + cameras[ci].propertyData["/lens/iris"]?.apertureStop.toFixed(1); - document.getElementById("shutterSpan").innerHTML = shutterString; - } + // ========== Zoom ========== - // =========== Auto Exposure Mode =========== + document.getElementById("zoomRange").value = + cameras[ci].propertyData["/lens/zoom"]?.normalised; + document.getElementById("zoomMMLabel").innerHTML = + cameras[ci].propertyData["/lens/zoom"]?.focalLength + "mm"; - if (!unsavedChanges.includes("AutoExposure")) { - let AEmodeSelect = document.getElementById("AEmodeDropDown"); - let AEtypeSelect = document.getElementById("AEtypeDropDown"); + // ========== Focus ========== - AEmodeSelect.value = cameras[ci].propertyData['/video/autoExposure']?.mode; - AEtypeSelect.value = cameras[ci].propertyData['/video/autoExposure']?.type; - } + document.getElementById("focusRange").value = + cameras[ci].propertyData["/lens/focus"]?.normalised; - // =========== COLOR CORRECTION ============= + // ========== ISO ========== + if (!unsavedChanges.includes("ISO")) { + if (cameras[ci].propertyData["/video/iso"]) + document.getElementById("ISOInput").value = + cameras[ci].propertyData["/video/iso"]?.iso; + } - // Lift - if (!unsavedChanges.includes("CC0")) { - let liftProps = cameras[ci].propertyData['/colorCorrection/lift']; - document.getElementsByClassName("CClumaLabel")[0].innerHTML = liftProps?.luma.toFixed(2); - document.getElementsByClassName("CCredLabel")[0].innerHTML = liftProps?.red.toFixed(2); - document.getElementsByClassName("CCgreenLabel")[0].innerHTML = liftProps?.green.toFixed(2); - document.getElementsByClassName("CCblueLabel")[0].innerHTML = liftProps?.blue.toFixed(2); - } + // ========== GAIN ========== - // Gamma - if (!unsavedChanges.includes("CC1")) { - let gammaProps = cameras[ci].propertyData['/colorCorrection/gamma']; - document.getElementsByClassName("CClumaLabel")[1].innerHTML = gammaProps?.luma.toFixed(2); - document.getElementsByClassName("CCredLabel")[1].innerHTML = gammaProps?.red.toFixed(2); - document.getElementsByClassName("CCgreenLabel")[1].innerHTML = gammaProps?.green.toFixed(2); - document.getElementsByClassName("CCblueLabel")[1].innerHTML = gammaProps?.blue.toFixed(2); - } + if (!unsavedChanges.includes("Gain")) { + let gainString = ""; + let gainInt = cameras[ci].propertyData["/video/gain"]?.gain; - // Gain - if (!unsavedChanges.includes("CC2")) { - let gainProps = cameras[ci].propertyData['/colorCorrection/gain']; - document.getElementsByClassName("CClumaLabel")[2].innerHTML = gainProps?.luma.toFixed(2); - document.getElementsByClassName("CCredLabel")[2].innerHTML = gainProps?.red.toFixed(2); - document.getElementsByClassName("CCgreenLabel")[2].innerHTML = gainProps?.green.toFixed(2); - document.getElementsByClassName("CCblueLabel")[2].innerHTML = gainProps?.blue.toFixed(2); + if (gainInt >= 0) { + gainString = "+" + gainInt + "db"; + } else { + gainString = gainInt + "db"; } - // Offset - if (!unsavedChanges.includes("CC3")) { - let offsetProps = cameras[ci].propertyData['/colorCorrection/offset']; - document.getElementsByClassName("CClumaLabel")[3].innerHTML = offsetProps?.luma.toFixed(2); - document.getElementsByClassName("CCredLabel")[3].innerHTML = offsetProps?.red.toFixed(2); - document.getElementsByClassName("CCgreenLabel")[3].innerHTML = offsetProps?.green.toFixed(2); - document.getElementsByClassName("CCblueLabel")[3].innerHTML = offsetProps?.blue.toFixed(2); - } + document.getElementById("gainSpan").innerHTML = gainString; + } - // Contrast - if (!unsavedChanges.includes("CC4")) { - let constrastProps = cameras[ci].propertyData['/colorCorrection/contrast']; - document.getElementById("CCcontrastPivotRange").value = constrastProps?.pivot; - document.getElementById("CCcontrastPivotLabel").innerHTML = constrastProps?.pivot.toFixed(2); - document.getElementById("CCcontrastAdjustRange").value = constrastProps?.adjust; - document.getElementById("CCcontrastAdjustLabel").innerHTML = parseInt(constrastProps?.adjust * 50)+"%"; + // ========== WHITE BALANCE =========== + + if (!unsavedChanges.includes("WB")) { + document.getElementById("whiteBalanceSpan").innerHTML = + cameras[ci].propertyData["/video/whiteBalance"]?.whiteBalance + "K"; + } + + if (!unsavedChanges.includes("WBT")) { + document.getElementById("whiteBalanceTintSpan").innerHTML = + cameras[ci].propertyData["/video/whiteBalanceTint"]?.whiteBalanceTint; + } + + // =========== ND ============= + + if (!unsavedChanges.includes("ND")) { + if (cameras[ci].propertyData["/video/ndFilter"]) { + document.getElementById("ndFilterSpan").innerHTML = + cameras[ci].propertyData["/video/ndFilter"]?.stop; + } else { + document.getElementById("ndFilterSpan").innerHTML = 0; + document.getElementById("ndFilterSpan").disabled = true; } - - // Color - if (!unsavedChanges.includes("CC5")) { - let colorProps = cameras[ci].propertyData['/colorCorrection/color']; - document.getElementById("CChueRange").value = colorProps?.hue; - document.getElementById("CCcolorHueLabel").innerHTML = parseInt((colorProps?.hue + 1) * 180)+"°"; - - document.getElementById("CCsaturationRange").value = colorProps?.saturation; - document.getElementById("CCcolorSatLabel").innerHTML = parseInt(colorProps?.saturation * 50)+"%"; - - let lumaContributionProps = cameras[ci].propertyData['/colorCorrection/lumaContribution']; - document.getElementById("CClumaContributionRange").value = lumaContributionProps?.lumaContribution; - document.getElementById("CCcolorLCLabel").innerHTML = parseInt(lumaContributionProps?.lumaContribution * 100)+"%"; + } + + // ============ Shutter ===================== + + if (!unsavedChanges.includes("Shutter")) { + let shutterString = "SS"; + let shutterObj = cameras[ci].propertyData["/video/shutter"]; + + if (shutterObj?.shutterSpeed) { + shutterString = "1/" + shutterObj.shutterSpeed; + } else if (shutterObj?.shutterAngle) { + var shangleString = (shutterObj.shutterAngle / 100).toFixed(1).toString(); + if (shangleString.indexOf(".0") > 0) { + shutterString = parseFloat(shangleString).toFixed(0) + "°"; + } else { + shutterString = shangleString + "°"; + } } - // ============ Footer Links =============== - document.getElementById("documentationLink").href = (cameras[ci].useHTTPS ? "https://" : "http://")+cameras[ci].hostname+"/control/documentation.html"; - document.getElementById("mediaManagerLink").href = (cameras[ci].useHTTPS ? "https://" : "http://")+cameras[ci].hostname; + document.getElementById("shutterSpan").innerHTML = shutterString; + } + + // =========== Auto Exposure Mode =========== + + if (!unsavedChanges.includes("AutoExposure")) { + let AEmodeSelect = document.getElementById("AEmodeDropDown"); + let AEtypeSelect = document.getElementById("AEtypeDropDown"); + + AEmodeSelect.value = cameras[ci].propertyData["/video/autoExposure"]?.mode; + AEtypeSelect.value = cameras[ci].propertyData["/video/autoExposure"]?.type; + } + + // =========== COLOR CORRECTION ============= + + // Lift + if (!unsavedChanges.includes("CC0")) { + let liftProps = cameras[ci].propertyData["/colorCorrection/lift"]; + document.getElementsByClassName("CClumaLabel")[0].innerHTML = + liftProps?.luma.toFixed(2); + document.getElementsByClassName("CCredLabel")[0].innerHTML = + liftProps?.red.toFixed(2); + document.getElementsByClassName("CCgreenLabel")[0].innerHTML = + liftProps?.green.toFixed(2); + document.getElementsByClassName("CCblueLabel")[0].innerHTML = + liftProps?.blue.toFixed(2); + } + + // Gamma + if (!unsavedChanges.includes("CC1")) { + let gammaProps = cameras[ci].propertyData["/colorCorrection/gamma"]; + document.getElementsByClassName("CClumaLabel")[1].innerHTML = + gammaProps?.luma.toFixed(2); + document.getElementsByClassName("CCredLabel")[1].innerHTML = + gammaProps?.red.toFixed(2); + document.getElementsByClassName("CCgreenLabel")[1].innerHTML = + gammaProps?.green.toFixed(2); + document.getElementsByClassName("CCblueLabel")[1].innerHTML = + gammaProps?.blue.toFixed(2); + } + + // Gain + if (!unsavedChanges.includes("CC2")) { + let gainProps = cameras[ci].propertyData["/colorCorrection/gain"]; + document.getElementsByClassName("CClumaLabel")[2].innerHTML = + gainProps?.luma.toFixed(2); + document.getElementsByClassName("CCredLabel")[2].innerHTML = + gainProps?.red.toFixed(2); + document.getElementsByClassName("CCgreenLabel")[2].innerHTML = + gainProps?.green.toFixed(2); + document.getElementsByClassName("CCblueLabel")[2].innerHTML = + gainProps?.blue.toFixed(2); + } + + // Offset + if (!unsavedChanges.includes("CC3")) { + let offsetProps = cameras[ci].propertyData["/colorCorrection/offset"]; + document.getElementsByClassName("CClumaLabel")[3].innerHTML = + offsetProps?.luma.toFixed(2); + document.getElementsByClassName("CCredLabel")[3].innerHTML = + offsetProps?.red.toFixed(2); + document.getElementsByClassName("CCgreenLabel")[3].innerHTML = + offsetProps?.green.toFixed(2); + document.getElementsByClassName("CCblueLabel")[3].innerHTML = + offsetProps?.blue.toFixed(2); + } + + // Contrast + if (!unsavedChanges.includes("CC4")) { + let constrastProps = cameras[ci].propertyData["/colorCorrection/contrast"]; + document.getElementById("CCcontrastPivotRange").value = + constrastProps?.pivot; + document.getElementById("CCcontrastPivotLabel").innerHTML = + constrastProps?.pivot.toFixed(2); + document.getElementById("CCcontrastAdjustRange").value = + constrastProps?.adjust; + document.getElementById("CCcontrastAdjustLabel").innerHTML = + parseInt(constrastProps?.adjust * 50) + "%"; + } + + // Color + if (!unsavedChanges.includes("CC5")) { + let colorProps = cameras[ci].propertyData["/colorCorrection/color"]; + document.getElementById("CChueRange").value = colorProps?.hue; + document.getElementById("CCcolorHueLabel").innerHTML = + parseInt((colorProps?.hue + 1) * 180) + "°"; + + document.getElementById("CCsaturationRange").value = colorProps?.saturation; + document.getElementById("CCcolorSatLabel").innerHTML = + parseInt(colorProps?.saturation * 50) + "%"; + + let lumaContributionProps = + cameras[ci].propertyData["/colorCorrection/lumaContribution"]; + document.getElementById("CClumaContributionRange").value = + lumaContributionProps?.lumaContribution; + document.getElementById("CCcolorLCLabel").innerHTML = + parseInt(lumaContributionProps?.lumaContribution * 100) + "%"; + } + + syncCCWheel(0); + syncCCWheel(1); + syncCCWheel(2); + syncCCWheel(3); + + // ============ Footer Links =============== + document.getElementById("documentationLink").href = + (cameras[ci].useHTTPS ? "https://" : "http://") + + cameras[ci].hostname + + "/control/documentation.html"; + document.getElementById("mediaManagerLink").href = + (cameras[ci].useHTTPS ? "https://" : "http://") + cameras[ci].hostname; } - // ============================================================================== // Called when the user changes tabs to a different camera function switchCamera(index) { - if (cameras[ci]) { - cameras[ci].active = false; - } + if (cameras[ci]) { + cameras[ci].active = false; + } - ci = index; + ci = index; - // Reset the Controls - document.getElementById("allCamerasContainer").innerHTML = defaultControlsHTML; + // Reset the Controls + document.getElementById("allCamerasContainer").innerHTML = + defaultControlsHTML; - // Update the UI + // Update the UI - for (var i = 0; i < 8; i++) { - if (i == ci) { - document.getElementsByClassName("cameraSwitchLabel")[i].classList.add("selectedCam"); - } else { - document.getElementsByClassName("cameraSwitchLabel")[i].classList.remove("selectedCam"); - } - } - - document.getElementById("cameraNumberLabel").innerHTML = "CAM"+(ci+1); - document.getElementById("cameraName").innerHTML = "CAMERA NAME"; - document.getElementById("hostnameInput").value = localStorage.getItem("camerahostname_"+ci.toString()); - if ( localStorage.getItem("camerasecurity_"+ci.toString()) === 'true' ) { - document.getElementById("secureCheckbox").checked = true - } - if (cameras[ci]) { - cameras[ci].active = true; + for (var i = 0; i < 8; i++) { + if (i == ci) { + document + .getElementsByClassName("cameraSwitchLabel") + [i].classList.add("selectedCam"); + } else { + document + .getElementsByClassName("cameraSwitchLabel") + [i].classList.remove("selectedCam"); } + } + + document.getElementById("cameraNumberLabel").innerHTML = "CAM" + (ci + 1); + document.getElementById("cameraName").innerHTML = "CAMERA NAME"; + document.getElementById("hostnameInput").value = localStorage.getItem( + "camerahostname_" + ci.toString() + ); + if (localStorage.getItem("camerasecurity_" + ci.toString()) === "true") { + document.getElementById("secureCheckbox").checked = true; + } + if (cameras[ci]) { + cameras[ci].active = true; + } } // For not-yet-implemented Color Correction UI function setCCMode(mode) { - if (mode == 0) { - // Lift - - } else if (mode == 1) { - // Gamma + if (mode == 0) { + // Lift + } else if (mode == 1) { + // Gamma + } else { + // Gain + } + for (var i = 0; i < 3; i++) { + if (i == mode) { + document + .getElementsByClassName("ccTabLabel") + [i].classList.add("selectedTab"); } else { - // Gain - - } - - for (var i = 0; i < 3; i++) { - if (i == mode) { - document.getElementsByClassName("ccTabLabel")[i].classList.add("selectedTab"); - } else { - document.getElementsByClassName("ccTabLabel")[i].classList.remove("selectedTab"); - } + document + .getElementsByClassName("ccTabLabel") + [i].classList.remove("selectedTab"); } + } } // Allows for changing WB/Tint displayed in the UI function swapWBMode() { - if (WBMode == 0) { - // Balance - document.getElementById("WBLabel").innerHTML = "TINT"; - document.getElementById("WBValueContainer").classList.add("dNone"); - document.getElementById("WBTintValueContainer").classList.remove("dNone"); - - WBMode = 1; - } else { - //Tint - document.getElementById("WBLabel").innerHTML = "BALANCE"; - document.getElementById("WBValueContainer").classList.remove("dNone"); - document.getElementById("WBTintValueContainer").classList.add("dNone"); - - WBMode = 0; - } + if (WBMode == 0) { + // Balance + document.getElementById("WBLabel").innerHTML = "TINT"; + document.getElementById("WBValueContainer").classList.add("dNone"); + document.getElementById("WBTintValueContainer").classList.remove("dNone"); + + WBMode = 1; + } else { + //Tint + document.getElementById("WBLabel").innerHTML = "BALANCE"; + document.getElementById("WBValueContainer").classList.remove("dNone"); + document.getElementById("WBTintValueContainer").classList.add("dNone"); + + WBMode = 0; + } } // Triggered by the button by those text boxes. Reads the info from the inputs and sends it to the camera. function manualAPICall() { - const requestRadioGET = document.getElementById("requestTypeGET"); + const requestRadioGET = document.getElementById("requestTypeGET"); - const requestEndpointText = document.getElementById("manualRequestEndpointLabel").value; - let requestData = ""; + const requestEndpointText = document.getElementById( + "manualRequestEndpointLabel" + ).value; + let requestData = ""; - try { - requestData = JSON.parse(document.getElementById("manualRequestBodyLabel").value); - } catch (err) { - document.getElementById("manualRequestResponseP").innerHTML = err; - } + try { + requestData = JSON.parse( + document.getElementById("manualRequestBodyLabel").value + ); + } catch (err) { + document.getElementById("manualRequestResponseP").innerHTML = err; + } - const requestMethod = (requestRadioGET.checked ? "GET" : "PUT"); - const requestURL = cameras[ci].APIAddress+requestEndpointText; + const requestMethod = requestRadioGET.checked ? "GET" : "PUT"; + const requestURL = cameras[ci].APIAddress + requestEndpointText; - let response = sendRequest(requestMethod,requestURL,requestData); - - document.getElementById("manualRequestResponseP").innerHTML = JSON.stringify(response); + let response = sendRequest(requestMethod, requestURL, requestData); + + document.getElementById("manualRequestResponseP").innerHTML = + JSON.stringify(response); } /* Control Calling Functions */ /* Makes the HTML cleaner. */ +function codecChange(selectElement) { + const selectedCodec = selectElement.value; + const supportedFormats = + cameras[ci].propertyData["/system/supportedFormats"].supportedFormats; + + const formatsForCodec = supportedFormats.filter((f) => + f.codecs.includes(selectedCodec) + ); + + let currentWidth = + cameras[ci].propertyData["/system/format"].recordResolution.width; + let currentHeight = + cameras[ci].propertyData["/system/format"].recordResolution.height; + + let formatToUse = formatsForCodec.find( + (f) => + f.recordResolution.width === currentWidth && + f.recordResolution.height === currentHeight + ); + + if (!formatToUse) { + formatToUse = formatsForCodec.reduce((prev, curr) => + curr.recordResolution.width * curr.recordResolution.height > + prev.recordResolution.width * prev.recordResolution.height + ? curr + : prev + ); + } + + cameras[ci].PUTdata("/system/format", { + codec: selectedCodec, + frameRate: cameras[ci].propertyData["/system/format"].frameRate, + recordResolution: formatToUse.recordResolution, + sensorResolution: formatToUse.sensorResolution, + }); + + const resSelect = document.getElementById("formatResSelect"); + resSelect.value = `${formatToUse.recordResolution.width}x${formatToUse.recordResolution.height}`; +} + +function resChange(selectElement) { + const selectedValue = selectElement.value; + + const [width, height] = selectedValue.split("x").map(Number); + + cameras[ci].PUTdata("/system/format", { + codec: cameras[ci].propertyData["/system/format"].codec, + frameRate: cameras[ci].propertyData["/system/format"].frameRate, + recordResolution: { + width: width, + height: height, + }, + sensorResolution: { + width: width, + height: height, + }, + }); +} + +function fpsChange(selectElement) { + const selectedValue = selectElement.value; + + const frameRate = parseFloat(selectedValue); + + cameras[ci].PUTdata("/system/format", { + codec: cameras[ci].propertyData["/system/format"].codec, + frameRate: frameRate.toString(), + recordResolution: + cameras[ci].propertyData["/system/format"].recordResolution, + sensorResolution: + cameras[ci].propertyData["/system/format"].sensorResolution, + }); +} function decreaseND() { - cameras[ci].PUTdata("/video/ndFilter",{stop: cameras[ci].propertyData['/video/ndFilter'].stop-2}); + cameras[ci].PUTdata("/video/ndFilter", { + stop: cameras[ci].propertyData["/video/ndFilter"].stop - 2, + }); } function increaseND() { - cameras[ci].PUTdata("/video/ndFilter",{stop: cameras[ci].propertyData['/video/ndFilter'].stop+2}); + cameras[ci].PUTdata("/video/ndFilter", { + stop: cameras[ci].propertyData["/video/ndFilter"].stop + 2, + }); } function decreaseGain() { - cameras[ci].PUTdata("/video/gain",{gain: cameras[ci].propertyData['/video/gain'].gain-2}); + cameras[ci].PUTdata("/video/gain", { + gain: cameras[ci].propertyData["/video/gain"].gain - 2, + }); } function increaseGain() { - cameras[ci].PUTdata("/video/gain",{gain: cameras[ci].propertyData['/video/gain'].gain+2}); + cameras[ci].PUTdata("/video/gain", { + gain: cameras[ci].propertyData["/video/gain"].gain + 2, + }); } function decreaseShutter() { - let cam = cameras[ci]; - - if ('shutterSpeed' in cam.propertyData['/video/shutter']) { - cam.PUTdata("/video/shutter", {"shutterSpeed": cam.propertyData['/video/shutter'].shutterSpeed+10}); - } else { - cam.PUTdata("/video/shutter", {"shutterAngle": cam.propertyData['/video/shutter'].shutterAngle-1000}); - } + let cam = cameras[ci]; + + if ("shutterSpeed" in cam.propertyData["/video/shutter"]) { + cam.PUTdata("/video/shutter", { + shutterSpeed: cam.propertyData["/video/shutter"].shutterSpeed + 10, + }); + } else { + cam.PUTdata("/video/shutter", { + shutterAngle: cam.propertyData["/video/shutter"].shutterAngle - 1000, + }); + } } function increaseShutter() { - let cam = cameras[ci]; - - if ('shutterSpeed' in cam.propertyData['/video/shutter']) { - cam.PUTdata("/video/shutter", {"shutterSpeed": cam.propertyData['/video/shutter'].shutterSpeed-10}); - } else { - cam.PUTdata("/video/shutter", {"shutterAngle": cam.propertyData['/video/shutter'].shutterAngle+1000}); - } + let cam = cameras[ci]; + + if ("shutterSpeed" in cam.propertyData["/video/shutter"]) { + cam.PUTdata("/video/shutter", { + shutterSpeed: cam.propertyData["/video/shutter"].shutterSpeed - 10, + }); + } else { + cam.PUTdata("/video/shutter", { + shutterAngle: cam.propertyData["/video/shutter"].shutterAngle + 1000, + }); + } } function handleShutterInput() { - let inputString = document.getElementById("shutterSpan").innerHTML; - - if (event.key === 'Enter') { - let cam = cameras[ci]; - - if ('shutterSpeed' in cam.propertyData['/video/shutter']) { - if (inputString.indexOf("1/") >= 0) { - cam.PUTdata("/video/shutter", {"shutterSpeed" :parseInt(inputString.substring(2))}); - } else { - cam.PUTdata("/video/shutter", {"shutterSpeed" :parseInt(inputString)}); - } - - } else { - cam.PUTdata("/video/shutter", {"shutterAngle": parseInt(parseFloat(inputString)*100)}); - } - - unsavedChanges = unsavedChanges.filter((e) => {return e !== "Shutter"}); + let inputString = document.getElementById("shutterSpan").innerHTML; + + if (event.key === "Enter") { + let cam = cameras[ci]; + + if ("shutterSpeed" in cam.propertyData["/video/shutter"]) { + if (inputString.indexOf("1/") >= 0) { + cam.PUTdata("/video/shutter", { + shutterSpeed: parseInt(inputString.substring(2)), + }); + } else { + cam.PUTdata("/video/shutter", { shutterSpeed: parseInt(inputString) }); + } } else { - unsavedChanges.push('Shutter'); + cam.PUTdata("/video/shutter", { + shutterAngle: parseInt(parseFloat(inputString) * 100), + }); } + + unsavedChanges = unsavedChanges.filter((e) => { + return e !== "Shutter"; + }); + } else { + unsavedChanges.push("Shutter"); + } } function decreaseWhiteBalance() { - cameras[ci].PUTdata("/video/whiteBalance", {whiteBalance: cameras[ci].propertyData['/video/whiteBalance'].whiteBalance-50}); + cameras[ci].PUTdata("/video/whiteBalance", { + whiteBalance: + cameras[ci].propertyData["/video/whiteBalance"].whiteBalance - 50, + }); } function increaseWhiteBalance() { - cameras[ci].PUTdata("/video/whiteBalance", {whiteBalance: cameras[ci].propertyData['/video/whiteBalance'].whiteBalance+50}); + cameras[ci].PUTdata("/video/whiteBalance", { + whiteBalance: + cameras[ci].propertyData["/video/whiteBalance"].whiteBalance + 50, + }); } function decreaseWhiteBalanceTint() { - cameras[ci].PUTdata("/video/whiteBalanceTint", {whiteBalanceTint: cameras[ci].propertyData['/video/whiteBalanceTint'].whiteBalanceTint-1}); + cameras[ci].PUTdata("/video/whiteBalanceTint", { + whiteBalanceTint: + cameras[ci].propertyData["/video/whiteBalanceTint"].whiteBalanceTint - 1, + }); } function increaseWhiteBalanceTint() { - cameras[ci].PUTdata("/video/whiteBalanceTint", {whiteBalanceTint: cameras[ci].propertyData['/video/whiteBalanceTint'].whiteBalanceTint+1}); + cameras[ci].PUTdata("/video/whiteBalanceTint", { + whiteBalanceTint: + cameras[ci].propertyData["/video/whiteBalanceTint"].whiteBalanceTint + 1, + }); } function presetInputHandler() { - let selectedPreset = document.getElementById("presetsDropDown").value; + let selectedPreset = document.getElementById("presetsDropDown").value; - cameras[ci].PUTdata("/presets/active", {preset: selectedPreset+".cset"}); + cameras[ci].PUTdata("/presets/active", { preset: selectedPreset + ".cset" }); - unsavedChanges = unsavedChanges.filter((e) => {return e !== "presets"}); + unsavedChanges = unsavedChanges.filter((e) => { + return e !== "presets"; + }); } function hostnameInputHandler() { - let newHostname = document.getElementById("hostnameInput").value; - - if (event.key === 'Enter') { - event.preventDefault; - unsavedChanges = unsavedChanges.filter((e) => {return e !== "Hostname"}); - initCamera(); - } else { - unsavedChanges.push('Hostname'); - } + let newHostname = document.getElementById("hostnameInput").value; + + if (event.key === "Enter") { + event.preventDefault; + unsavedChanges = unsavedChanges.filter((e) => { + return e !== "Hostname"; + }); + initCamera(); + } else { + unsavedChanges.push("Hostname"); + } } function AEmodeInputHandler() { - let AEmode = document.getElementById("AEmodeDropDown").value; - let AEtype = document.getElementById("AEtypeDropDown").value; + let AEmode = document.getElementById("AEmodeDropDown").value; + let AEtype = document.getElementById("AEtypeDropDown").value; - cameras[ci].PUTdata("/video/autoExposure", {mode: AEmode, type: AEtype}); + cameras[ci].PUTdata("/video/autoExposure", { mode: AEmode, type: AEtype }); - unsavedChanges = unsavedChanges.filter((e) => {return e !== "AutoExposure"}); + unsavedChanges = unsavedChanges.filter((e) => { + return e !== "AutoExposure"; + }); } function ISOInputHandler() { - let ISOInput = document.getElementById("ISOInput"); - - if (event.key === 'Enter') { - event.preventDefault; - cameras[ci].PUTdata("/video/iso", {iso: parseInt(ISOInput.value)}) - unsavedChanges = unsavedChanges.filter((e) => {return e !== "ISO"}); - } else { - unsavedChanges.push('ISO'); - } + let ISOInput = document.getElementById("ISOInput"); + + if (event.key === "Enter") { + event.preventDefault; + cameras[ci].PUTdata("/video/iso", { iso: parseInt(ISOInput.value) }); + unsavedChanges = unsavedChanges.filter((e) => { + return e !== "ISO"; + }); + } else { + unsavedChanges.push("ISO"); + } } // 0: lift, 1: gamma, 2: gain, 3: offset, 4: contrast, 5: color & LC function CCInputHandler(which) { - if (event.key === 'Enter') { - event.preventDefault; - setCCFromUI(which); - } else { - unsavedChanges.push('CC'+which); - } + if (event.key === "Enter") { + event.preventDefault; + setCCFromUI(which); + } else { + unsavedChanges.push("CC" + which); + } } function NDFilterInputHandler() { - if (event.key === 'Enter') { - event.preventDefault; - cameras[ci].PUTdata("/video/ndFilter", {stop: parseInt(document.getElementById("ndFilterSpan").innerHTML)}) - unsavedChanges = unsavedChanges.filter((e) => {return e !== "ND"}); - } else { - unsavedChanges.push('ND'); - } + if (event.key === "Enter") { + event.preventDefault; + cameras[ci].PUTdata("/video/ndFilter", { + stop: parseInt(document.getElementById("ndFilterSpan").innerHTML), + }); + unsavedChanges = unsavedChanges.filter((e) => { + return e !== "ND"; + }); + } else { + unsavedChanges.push("ND"); + } } function GainInputHandler() { - if (event.key === 'Enter') { - event.preventDefault; - cameras[ci].PUTdata("/video/gain", {gain: parseInt(document.getElementById("gainSpan").innerHTML)}) - unsavedChanges = unsavedChanges.filter((e) => {return e !== "Gain"}); - } else { - unsavedChanges.push('Gain'); - } + if (event.key === "Enter") { + event.preventDefault; + cameras[ci].PUTdata("/video/gain", { + gain: parseInt(document.getElementById("gainSpan").innerHTML), + }); + unsavedChanges = unsavedChanges.filter((e) => { + return e !== "Gain"; + }); + } else { + unsavedChanges.push("Gain"); + } } function WBInputHandler() { - if (event.key === 'Enter') { - event.preventDefault; - cameras[ci].PUTdata("/video/whiteBalance", {whiteBalance: parseInt(document.getElementById("whiteBalanceSpan").innerHTML)}) - unsavedChanges = unsavedChanges.filter((e) => {return e !== "WB"}); - } else { - unsavedChanges.push('WB'); - } + if (event.key === "Enter") { + event.preventDefault; + cameras[ci].PUTdata("/video/whiteBalance", { + whiteBalance: parseInt( + document.getElementById("whiteBalanceSpan").innerHTML + ), + }); + unsavedChanges = unsavedChanges.filter((e) => { + return e !== "WB"; + }); + } else { + unsavedChanges.push("WB"); + } } function WBTInputHandler() { - if (event.key === 'Enter') { - event.preventDefault; - cameras[ci].PUTdata("/video/whiteBalanceTint", {whiteBalanceTint: parseInt(document.getElementById("whiteBalanceTintSpan").innerHTML)}) - unsavedChanges = unsavedChanges.filter((e) => {return e !== "WBT"}); - } else { - unsavedChanges.push('WBT'); - } + if (event.key === "Enter") { + event.preventDefault; + cameras[ci].PUTdata("/video/whiteBalanceTint", { + whiteBalanceTint: parseInt( + document.getElementById("whiteBalanceTintSpan").innerHTML + ), + }); + unsavedChanges = unsavedChanges.filter((e) => { + return e !== "WBT"; + }); + } else { + unsavedChanges.push("WBT"); + } } // 0: lift, 1: gamma, 2: gain, 3: offset function setCCFromUI(which) { - if (which < 4) { - var lumaFloat = parseFloat(document.getElementsByClassName("CClumaLabel")[which].innerHTML); - var redFloat = parseFloat(document.getElementsByClassName("CCredLabel")[which].innerHTML); - var greenFloat = parseFloat(document.getElementsByClassName("CCgreenLabel")[which].innerHTML); - var blueFloat = parseFloat(document.getElementsByClassName("CCblueLabel")[which].innerHTML); - - var ccobject = {"red": redFloat, "green": greenFloat, "blue": blueFloat, "luma": lumaFloat}; - } - - if (which == 0) { - cameras[ci].PUTdata("/colorCorrection/lift", ccobject); - } else if (which == 1) { - cameras[ci].PUTdata("/colorCorrection/gamma", ccobject); - } else if (which == 2) { - cameras[ci].PUTdata("/colorCorrection/gain", ccobject); - } else if (which == 3) { - cameras[ci].PUTdata("/colorCorrection/offset", ccobject); - } else if (which == 4) { - let pivotFloat = parseFloat(document.getElementById("CCcontrastPivotLabel").innerHTML); - let adjustInt = parseInt(document.getElementById("CCcontrastAdjustLabel").innerHTML); - - let adjustFloat = adjustInt/50.0; - - cameras[ci].PUTdata("/colorCorrection/contrast", {pivot: pivotFloat, adjust: adjustFloat}); - } else { - let hueInt = parseInt(document.getElementById("CCcolorHueLabel").innerHTML); - let satInt = parseInt(document.getElementById("CCcolorSatLabel").innerHTML); - let lumCoInt = parseInt(document.getElementById("CCcolorLCLabel").innerHTML); - - let hueFloat = (hueInt/180.0) - 1.0; - let satFloat = satInt/50.0; - let lumCoFloat = lumCoInt/100.0; - - cameras[ci].PUTdata("/colorCorrection/color", {hue: hueFloat, saturation: satFloat}); - cameras[ci].PUTdata("/colorCorrection/lumaContribution", {lumaContribution: lumCoFloat}); - } - - unsavedChanges = unsavedChanges.filter((e) => {return !e.includes("CC"+which)}); + if (which < 4) { + var lumaFloat = parseFloat( + document.getElementsByClassName("CClumaLabel")[which].innerHTML + ); + var redFloat = parseFloat( + document.getElementsByClassName("CCredLabel")[which].innerHTML + ); + var greenFloat = parseFloat( + document.getElementsByClassName("CCgreenLabel")[which].innerHTML + ); + var blueFloat = parseFloat( + document.getElementsByClassName("CCblueLabel")[which].innerHTML + ); + + var ccobject = { + red: redFloat, + green: greenFloat, + blue: blueFloat, + luma: lumaFloat, + }; + } + + if (which == 0) { + cameras[ci].PUTdata("/colorCorrection/lift", ccobject); + } else if (which == 1) { + cameras[ci].PUTdata("/colorCorrection/gamma", ccobject); + } else if (which == 2) { + cameras[ci].PUTdata("/colorCorrection/gain", ccobject); + } else if (which == 3) { + cameras[ci].PUTdata("/colorCorrection/offset", ccobject); + } else if (which == 4) { + let pivotFloat = parseFloat( + document.getElementById("CCcontrastPivotLabel").innerHTML + ); + let adjustInt = parseInt( + document.getElementById("CCcontrastAdjustLabel").innerHTML + ); + + let adjustFloat = adjustInt / 50.0; + + cameras[ci].PUTdata("/colorCorrection/contrast", { + pivot: pivotFloat, + adjust: adjustFloat, + }); + } else { + let hueInt = parseInt(document.getElementById("CCcolorHueLabel").innerHTML); + let satInt = parseInt(document.getElementById("CCcolorSatLabel").innerHTML); + let lumCoInt = parseInt( + document.getElementById("CCcolorLCLabel").innerHTML + ); + + let hueFloat = hueInt / 180.0 - 1.0; + let satFloat = satInt / 50.0; + let lumCoFloat = lumCoInt / 100.0; + + cameras[ci].PUTdata("/colorCorrection/color", { + hue: hueFloat, + saturation: satFloat, + }); + cameras[ci].PUTdata("/colorCorrection/lumaContribution", { + lumaContribution: lumCoFloat, + }); + } + + unsavedChanges = unsavedChanges.filter((e) => { + return !e.includes("CC" + which); + }); } // Reset Color Correction Values // 0: lift, 1: gamma, 2: gain, 3: offset, 4: contrast, 5: color & LC function resetCC(which) { - if (which == 0) { - cameras[ci].PUTdata("/colorCorrection/lift", {"red": 0.0, "green": 0.0, "blue": 0.0, "luma": 0.0}); - } else if (which == 1) { - cameras[ci].PUTdata("/colorCorrection/gamma", {"red": 0.0, "green": 0.0, "blue": 0.0, "luma": 0.0}); - } else if (which == 2) { - cameras[ci].PUTdata("/colorCorrection/gain", {"red": 1.0, "green": 1.0, "blue": 1.0, "luma": 1.0}); - } else if (which == 3) { - cameras[ci].PUTdata("/colorCorrection/offset", {"red": 0.0, "green": 0.0, "blue": 0.0, "luma": 0.0}); - } else if (which == 4) { - cameras[ci].PUTdata("/colorCorrection/contrast", {"pivot": 0.5, "adjust": 1.0}); - } else if (which == 5) { - cameras[ci].PUTdata("/colorCorrection/color", {"hue": 0.0, "saturation": 1.0}); - cameras[ci].PUTdata("/colorCorrection/lumaContribution", {"lumaContribution": 1.0}); - } - - unsavedChanges = unsavedChanges.filter((e) => {return !e.includes("CC"+which)}); + if (which == 0) { + cameras[ci].PUTdata("/colorCorrection/lift", { + red: 0.0, + green: 0.0, + blue: 0.0, + luma: 0.0, + }); + } else if (which == 1) { + cameras[ci].PUTdata("/colorCorrection/gamma", { + red: 0.0, + green: 0.0, + blue: 0.0, + luma: 0.0, + }); + } else if (which == 2) { + cameras[ci].PUTdata("/colorCorrection/gain", { + red: 1.0, + green: 1.0, + blue: 1.0, + luma: 1.0, + }); + } else if (which == 3) { + cameras[ci].PUTdata("/colorCorrection/offset", { + red: 0.0, + green: 0.0, + blue: 0.0, + luma: 0.0, + }); + } else if (which == 4) { + cameras[ci].PUTdata("/colorCorrection/contrast", { + pivot: 0.5, + adjust: 1.0, + }); + } else if (which == 5) { + cameras[ci].PUTdata("/colorCorrection/color", { + hue: 0.0, + saturation: 1.0, + }); + cameras[ci].PUTdata("/colorCorrection/lumaContribution", { + lumaContribution: 1.0, + }); + } + + unsavedChanges = unsavedChanges.filter((e) => { + return !e.includes("CC" + which); + }); } // Triggered by the Loop and Single Clip buttons function loopHandler(callerString) { - let playbackState = cameras[ci].propertyData['/transports/0/playback']; - - if (callerString === "Loop") { - playbackState.loop = !playbackState.loop; - } else if (callerString === "Single Clip") { - playbackState.singleClip = !playbackState.singleClip; - } + let playbackState = cameras[ci].propertyData["/transports/0/playback"]; + + if (callerString === "Loop") { + playbackState.loop = !playbackState.loop; + } else if (callerString === "Single Clip") { + playbackState.singleClip = !playbackState.singleClip; + } - cameras[ci].PUTdata("/transports/0/playback", playbackState); + cameras[ci].PUTdata("/transports/0/playback", playbackState); } /* Helper Functions */ function parseTimecode(timecodeBCD) { - let noDropFrame = timecodeBCD & 0b01111111111111111111111111111111; // The first bit of the timecode is 1 if "Drop Frame Timecode" is on. We don't want to include that in the display. - let decimalTCInt = parseInt(noDropFrame.toString(16), 10); // Convert the BCD number into base ten - let decimalTCString = decimalTCInt.toString().padStart(8, '0'); // Convert the base ten number to a string eight characters long - let finalTCString = decimalTCString.match(/.{1,2}/g).join(':'); // Put colons between every two characters - return finalTCString; + let noDropFrame = timecodeBCD & 0b01111111111111111111111111111111; // The first bit of the timecode is 1 if "Drop Frame Timecode" is on. We don't want to include that in the display. + let decimalTCInt = parseInt(noDropFrame.toString(16), 10); // Convert the BCD number into base ten + let decimalTCString = decimalTCInt.toString().padStart(8, "0"); // Convert the base ten number to a string eight characters long + let finalTCString = decimalTCString.match(/.{1,2}/g).join(":"); // Put colons between every two characters + return finalTCString; }