From cb75d65a0dfd6beefbc47aadee7d04609b465e99 Mon Sep 17 00:00:00 2001 From: Valerii Kamelkov Date: Sun, 14 Sep 2025 15:26:58 +0200 Subject: [PATCH 1/3] format select --- BMDevice.js | 507 ++++++++++--------- index.html | 697 ++++++++++++++------------ web-ui.js | 1369 +++++++++++++++++++++++++++++++++------------------ 3 files changed, 1542 insertions(+), 1031 deletions(-) diff --git a/BMDevice.js b/BMDevice.js index b20e9d8..0cbe981 100644 --- a/BMDevice.js +++ b/BMDevice.js @@ -17,272 +17,299 @@ // 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 = 300; + // 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; + } + + // Обновляем UI не чаще, чем раз в UI_UPDATE_INTERVAL мс + 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..9bfc340 100644 --- a/index.html +++ b/index.html @@ -2,348 +2,429 @@ - - - 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/web-ui.js b/web-ui.js index 83b1df5..c3017a5 100644 --- a/web-ui.js +++ b/web-ui.js @@ -4,13 +4,12 @@ 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; @@ -18,638 +17,1042 @@ var unsavedChanges = []; // 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; + } } - // 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; - } - } 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)"; + // 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 { + // Something has gone wrong, tell the user + document.getElementById("connectionErrorSpan").innerHTML = + response.statusText; } - - unsavedChanges = unsavedChanges.filter((e) => {return e !== "Hostname"}); + } 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"; + }); } // =============================== UI Updater ================================== // ============================================================================= function updateUIAll() { - // ========== Camera Name ========== - - document.getElementById("cameraName").innerHTML = cameras[ci].name; - - // ========== Hostname ========== - - if (!unsavedChanges.includes("Hostname")) { - document.getElementById("hostnameInput").value = cameras[ci].hostname; - } - - // ========== Format ========== - - 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"; - - // ========== 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"); + // ========== Camera Name ========== + + document.getElementById("cameraName").innerHTML = cameras[ci].name; + + // ========== Hostname ========== + + if (!unsavedChanges.includes("Hostname")) { + document.getElementById("hostnameInput").value = cameras[ci].hostname; + } + + // ========== Format ========== + // Селекты + const codecSelect = document.getElementById("formatCodecSelect"); + const resSelect = document.getElementById("formatResSelect"); + const fpsSelect = document.getElementById("formatFPSSelect"); + + // Объект камеры + const camera = cameras[ci]; // твой объект камеры + + // Функция для обновления списка кодеков + function updateCodecs() { + const codecs = camera.propertyData[ + "/system/supportedFormats" + ].supportedFormats + .map((f) => f.codecs) + .flat() + .map((c) => c); + + // убираем дубликаты + const uniqueCodecs = [...new Set(codecs)]; + + codecSelect.innerHTML = uniqueCodecs + .map((c) => ``) + .join(""); + + // выбрать текущий codec + const currentCodec = camera.propertyData["/system/format"]?.codec; + + codecSelect.value = currentCodec; + + updateResolutions(); + } + + // Функция для обновления списка разрешений в зависимости от выбранного кодека + function updateResolutions() { + const selectedCodec = codecSelect.value; // обратно к формату объекта + const formats = + camera.propertyData["/system/supportedFormats"].supportedFormats; + + // фильтруем форматы по выбранному кодеку + const availableRes = formats + .filter((f) => f.codecs.includes(selectedCodec)) + .map((f) => `${f.recordResolution.width}x${f.recordResolution.height}`); + + // убираем дубликаты + const uniqueRes = [...new Set(availableRes)]; + + resSelect.innerHTML = uniqueRes + .map((r) => ``) + .join(""); + + // выбрать текущее разрешение + const currentRes = camera.propertyData["/system/format"]?.recordResolution; + resSelect.value = `${currentRes?.width}x${currentRes?.height}`; + + updateFPS(); + } + + // Функция для обновления FPS в зависимости от выбранного кодека и разрешения + function updateFPS() { + const selectedCodec = codecSelect.value; + const selectedRes = resSelect.value.split("x").map(Number); + + 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] + ); + + const fpsOptions = matchingFormat?.frameRates || []; + fpsSelect.innerHTML = fpsOptions + .map((fps) => ``) + .join(""); + + // выбрать текущий fps + const currentFPS = camera.propertyData["/system/format"]?.frameRate; + if (fpsOptions.includes(currentFPS)) { + fpsSelect.value = `${currentFPS} fps`; } - - 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); - }); + } + + // Обработчики изменения селектов + codecSelect.addEventListener("change", updateResolutions); + resSelect.addEventListener("change", updateFPS); + + // Инициализация + updateCodecs(); + + // const codecSelect = document.getElementById("formatCodecSelect"); + // const resSelect = document.getElementById("formatResSelect"); + // const fpsSelect = document.getElementById("formatFPSSelect"); + + // document.getElementById("formatCodecSelect").innerHTML = ` + // + // ${cameras[ci].propertyData[ + // "/system/supportedFormats" + // ]?.supportedFormats[0].codecs.map( + // (el) => + // `}` + // )}`; + + // let resObj = cameras[ci].propertyData["/system/format"]?.recordResolution; + // document.getElementById("formatResSelect").innerHTML = ` ${cameras[ci].propertyData[ + // "/system/supportedFormats" + // ]?.supportedFormats[0].codecs.map( + // (el) => + // `}` + // )}`; + // document.getElementById("formatFPSSelect").innerHTML = ``; + + // ========== 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); + }); // ========== Active Preset ========== - var presetsList = document.getElementById("presetsDropDown"); + var presetsList = document.getElementById("presetsDropDown"); - presetsList.childNodes.forEach((child) => { - if (child.nodeName == 'OPTION' && (child.value+".cset") == cameras[ci].propertyData['/presets/active']?.preset) { - child.selected=true - } else { - child.selected=false - } - }) + presetsList.childNodes.forEach((child) => { + if ( + child.nodeName == "OPTION" && + child.value + ".cset" == + cameras[ci].propertyData["/presets/active"]?.preset + ) { + child.selected = true; + } else { + child.selected = false; + } + }); + } - } - - // ========== Iris ========== - - document.getElementById("irisRange").value = cameras[ci].propertyData['/lens/iris']?.normalised; - document.getElementById("apertureStopsLabel").innerHTML = cameras[ci].propertyData['/lens/iris']?.apertureStop.toFixed(1); - - // ========== Zoom ========== - - document.getElementById("zoomRange").value = cameras[ci].propertyData['/lens/zoom']?.normalised; - document.getElementById("zoomMMLabel").innerHTML = cameras[ci].propertyData['/lens/zoom']?.focalLength +"mm"; - - // ========== Focus ========== - - document.getElementById("focusRange").value = cameras[ci].propertyData['/lens/focus']?.normalised; - - // ========== ISO ========== - if (!unsavedChanges.includes("ISO")) { - if (cameras[ci].propertyData['/video/iso']) - document.getElementById("ISOInput").value = cameras[ci].propertyData['/video/iso']?.iso; - } - - // ========== GAIN ========== - - if (!unsavedChanges.includes("Gain")) { - let gainString = ""; - let gainInt = cameras[ci].propertyData['/video/gain']?.gain - - if (gainInt >= 0) { - gainString = "+"+gainInt+"db" - } else { - gainString = gainInt+"db" - } - - document.getElementById("gainSpan").innerHTML = gainString; - } - - // ========== WHITE BALANCE =========== + // ========== Iris ========== - 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; - } + document.getElementById("irisRange").value = + cameras[ci].propertyData["/lens/iris"]?.normalised; + document.getElementById("apertureStopsLabel").innerHTML = + cameras[ci].propertyData["/lens/iris"]?.apertureStop.toFixed(1); - // =========== 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; - } - } + // ========== Zoom ========== - // ============ Shutter ===================== + document.getElementById("zoomRange").value = + cameras[ci].propertyData["/lens/zoom"]?.normalised; + document.getElementById("zoomMMLabel").innerHTML = + cameras[ci].propertyData["/lens/zoom"]?.focalLength + "mm"; - if (!unsavedChanges.includes("Shutter")) { - let shutterString = "SS" - let shutterObj = cameras[ci].propertyData['/video/shutter']; + // ========== Focus ========== - 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("focusRange").value = + cameras[ci].propertyData["/lens/focus"]?.normalised; - document.getElementById("shutterSpan").innerHTML = shutterString; - } + // ========== ISO ========== + if (!unsavedChanges.includes("ISO")) { + if (cameras[ci].propertyData["/video/iso"]) + document.getElementById("ISOInput").value = + cameras[ci].propertyData["/video/iso"]?.iso; + } - // =========== Auto Exposure Mode =========== + // ========== GAIN ========== - if (!unsavedChanges.includes("AutoExposure")) { - let AEmodeSelect = document.getElementById("AEmodeDropDown"); - let AEtypeSelect = document.getElementById("AEtypeDropDown"); + if (!unsavedChanges.includes("Gain")) { + let gainString = ""; + let gainInt = cameras[ci].propertyData["/video/gain"]?.gain; - AEmodeSelect.value = cameras[ci].propertyData['/video/autoExposure']?.mode; - AEtypeSelect.value = cameras[ci].propertyData['/video/autoExposure']?.type; + if (gainInt >= 0) { + gainString = "+" + gainInt + "db"; + } else { + gainString = gainInt + "db"; } - // =========== COLOR CORRECTION ============= + document.getElementById("gainSpan").innerHTML = gainString; + } - // 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); - } + // ========== WHITE BALANCE =========== - // 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("WB")) { + document.getElementById("whiteBalanceSpan").innerHTML = + cameras[ci].propertyData["/video/whiteBalance"]?.whiteBalance + "K"; + } - // 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 (!unsavedChanges.includes("WBT")) { + document.getElementById("whiteBalanceTintSpan").innerHTML = + cameras[ci].propertyData["/video/whiteBalanceTint"]?.whiteBalanceTint; + } - // 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); - } + // =========== ND ============= - // 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)+"%"; + 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) + "%"; + } + + // ============ 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; - } - - ci = index; + if (cameras[ci]) { + cameras[ci].active = false; + } - // Reset the Controls - document.getElementById("allCamerasContainer").innerHTML = defaultControlsHTML; + ci = index; - // Update the UI + // Reset the Controls + document.getElementById("allCamerasContainer").innerHTML = + defaultControlsHTML; - 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"); - } - } + // Update the UI - 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; } From 1e4b77f0fcfa0b7366fc6177e142235d57421a94 Mon Sep 17 00:00:00 2001 From: Valerii Kamelkov Date: Sun, 14 Sep 2025 15:31:31 +0200 Subject: [PATCH 2/3] ! --- BMDevice.js | 1 - web-ui.js | 53 +---------------------------------------------------- 2 files changed, 1 insertion(+), 53 deletions(-) diff --git a/BMDevice.js b/BMDevice.js index 0cbe981..ba916d6 100644 --- a/BMDevice.js +++ b/BMDevice.js @@ -86,7 +86,6 @@ class BMDevice { this.propertyData[messageData.property] = messageData.value; } - // Обновляем UI не чаще, чем раз в UI_UPDATE_INTERVAL мс if (this.active) { let now = Date.now(); if (now - lastUIUpdate >= UI_UPDATE_INTERVAL) { diff --git a/web-ui.js b/web-ui.js index c3017a5..7415afa 100644 --- a/web-ui.js +++ b/web-ui.js @@ -119,14 +119,12 @@ function updateUIAll() { .flat() .map((c) => c); - // убираем дубликаты const uniqueCodecs = [...new Set(codecs)]; codecSelect.innerHTML = uniqueCodecs .map((c) => ``) .join(""); - // выбрать текущий codec const currentCodec = camera.propertyData["/system/format"]?.codec; codecSelect.value = currentCodec; @@ -134,32 +132,27 @@ function updateUIAll() { updateResolutions(); } - // Функция для обновления списка разрешений в зависимости от выбранного кодека function updateResolutions() { - const selectedCodec = codecSelect.value; // обратно к формату объекта + const selectedCodec = codecSelect.value; const formats = camera.propertyData["/system/supportedFormats"].supportedFormats; - // фильтруем форматы по выбранному кодеку const availableRes = formats .filter((f) => f.codecs.includes(selectedCodec)) .map((f) => `${f.recordResolution.width}x${f.recordResolution.height}`); - // убираем дубликаты const uniqueRes = [...new Set(availableRes)]; resSelect.innerHTML = uniqueRes .map((r) => ``) .join(""); - // выбрать текущее разрешение const currentRes = camera.propertyData["/system/format"]?.recordResolution; resSelect.value = `${currentRes?.width}x${currentRes?.height}`; updateFPS(); } - // Функция для обновления FPS в зависимости от выбранного кодека и разрешения function updateFPS() { const selectedCodec = codecSelect.value; const selectedRes = resSelect.value.split("x").map(Number); @@ -178,55 +171,17 @@ function updateUIAll() { .map((fps) => ``) .join(""); - // выбрать текущий fps const currentFPS = camera.propertyData["/system/format"]?.frameRate; if (fpsOptions.includes(currentFPS)) { fpsSelect.value = `${currentFPS} fps`; } } - // Обработчики изменения селектов codecSelect.addEventListener("change", updateResolutions); resSelect.addEventListener("change", updateFPS); - // Инициализация updateCodecs(); - // const codecSelect = document.getElementById("formatCodecSelect"); - // const resSelect = document.getElementById("formatResSelect"); - // const fpsSelect = document.getElementById("formatFPSSelect"); - - // document.getElementById("formatCodecSelect").innerHTML = ` - // - // ${cameras[ci].propertyData[ - // "/system/supportedFormats" - // ]?.supportedFormats[0].codecs.map( - // (el) => - // `}` - // )}`; - - // let resObj = cameras[ci].propertyData["/system/format"]?.recordResolution; - // document.getElementById("formatResSelect").innerHTML = ` ${cameras[ci].propertyData[ - // "/system/supportedFormats" - // ]?.supportedFormats[0].codecs.map( - // (el) => - // `}` - // )}`; - // document.getElementById("formatFPSSelect").innerHTML = ``; - // ========== Recording State ========== if (cameras[ci].propertyData["/transports/0/record"]?.recording) { @@ -611,25 +566,21 @@ function codecChange(selectElement) { 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 > @@ -639,7 +590,6 @@ function codecChange(selectElement) { ); } - // Применяем новый формат cameras[ci].PUTdata("/system/format", { codec: selectedCodec, frameRate: cameras[ci].propertyData["/system/format"].frameRate, @@ -647,7 +597,6 @@ function codecChange(selectElement) { sensorResolution: formatToUse.sensorResolution, }); - // Обновляем селект разрешений const resSelect = document.getElementById("formatResSelect"); resSelect.value = `${formatToUse.recordResolution.width}x${formatToUse.recordResolution.height}`; } From f74bd87f12cb42b80ab5c7606c02092c98cd8be5 Mon Sep 17 00:00:00 2001 From: Valerii Kamelkov Date: Fri, 19 Sep 2025 00:55:41 +0200 Subject: [PATCH 3/3] 1 --- BMDevice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BMDevice.js b/BMDevice.js index ba916d6..0b75000 100644 --- a/BMDevice.js +++ b/BMDevice.js @@ -62,7 +62,7 @@ class BMDevice { // Get a self object for accessing within callback fns var self = this; let lastUIUpdate = 0; - const UI_UPDATE_INTERVAL = 300; + const UI_UPDATE_INTERVAL = 50; // Set the onmessage behavior this.ws.onmessage = (event) => { // Parse the event's data as JSON