Hide and Seek

This commit is contained in:
DylanSpeiser 2024-07-02 14:53:25 -07:00
parent 68279ff484
commit 0294bdfed8
7 changed files with 492 additions and 868 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
.DS_Store
/.vscode

View file

@ -1,483 +0,0 @@
/* Blackmagic Camera Control JS Class
Written based on the Camera Control
API Documentation from Blackmagic's
Developer info website.
(c) Dylan Speiser 2024
github.com/DylanSpeiser
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published.
This program is distributed in the hope that it will be useful but without
any warranty; without even the implied warranty of merchantability or fitness
for a particular purpose as specified by the License, which you should consult
for more details at LICENSE.txt in this repository.
*/
class BMDCamera {
// Pretty name and network hostname (strings)
name;
hostname;
APIAddress;
// Codec and Video Format (JSON object)
format;
// Current Transport Mode (string)
transportMode;
// Current Playback state (JSON object)
playbackState;
// Current Record state (JSON object)
recordState;
// Timecode (JSON Object)
timecode;
// Presets (JSON object)
presets;
activePreset;
// Iris (floats)
apertureStop;
apertureNormalised;
// Zoom (floats)
zoomMM;
zoomNormalised;
// Focus (float)
focusNormalised;
// ISO (int)
ISO;
// Gain (int)
gain;
// White Balance (ints)
WhiteBalance;
WhiteBalanceTint;
// ND Filter (int, string)
NDStop;
NDMode;
// Shutter (JSON object)
shutter;
// has to be an object because it either returns with shutterSpeed or shutterAngle
// AE Mode (JSON Object)
AutoExposureMode;
// Basic Color Correction (JSON objects w/ RGBL)
CClift;
CCgamma;
CCgain;
CCoffset;
// Other Color Correction (JSON objects w/ 2 numbers)
CCcontrast;
CCcolor;
CClumacontribution;
// Keep track of unimplemented functions on the camera (array of endpoint strings)
UnimplementedFunctionality = [];
// UI refreshing functions, will get called after every get method to keep the UI updated,
// 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.
static updateUIAll() {};
static updateUIname() {};
static updateUIhostname() {};
static updateUIFormat() {};
static updateUITransportMode() {};
static updateUIPlaybackState() {};
static updateUIRecordState() {};
static updateUITimecode() {};
static updateUIPresets() {};
static updateUIActivePreset() {};
static updateUIAperture() {};
static updateUIZoom() {};
static updateUIFocus() {};
static updateUIISO() {};
static updateUIgain() {};
static updateUIWhiteBalance() {};
static updateUINDStop() {};
static updateUIshutter() {};
static updateUIAutoExposureMode() {};
static updateUIColorCorrection() {};
static updateUILinks() {};
// ============= CONSTRUCTOR ================
constructor(hostname) {
this.hostname = hostname;
this.APIAddress = "http://"+hostname+"/control/api/v1";
this.name = this.hostname.replace(".local","").replaceAll("-"," ");
}
// Wrapper for API call, returns the JSON object from the camera
async pullData(endpoint) {
// Ask the camera a question
let response;
// Only send the request if it's not an Unimplemented Function
if (this.UnimplementedFunctionality.indexOf(endpoint) < 0) {
response = await sendRequest("GET",this.APIAddress+endpoint,"");
} else {
response = "Unimplemented";
}
// Check for unimplemented features
if (response.status == 501) {
this.UnimplementedFunctionality.push(endpoint);
response = "Unimplemented";
}
return response
}
// Wrapper for API call, sends data to the camera.
async pushData(endpoint, data) {
return await sendRequest("PUT",this.APIAddress+endpoint,data);
}
// =============== GETTERS ==================
// name, hostname, APIaddress handled by constructor
// since these are all asynchronous, they will return promises to use with await or .then()
async getFormat() {
this.format = await this.pullData("/system/format");
BMDCamera.updateUIFormat();
return this.format;
}
async getTransportMode() {
this.transportMode = await this.pullData("/transports/0");
BMDCamera.updateUITransportMode();
return this.transportMode;
}
async getPlaybackState() {
this.playbackState = await this.pullData("/transports/0/playback");
BMDCamera.updateUIPlaybackState();
return this.playbackState;
}
async getRecordState() {
this.recordState = await this.pullData("/transports/0/record");
BMDCamera.updateUIRecordState();
return this.recordState;
}
async getTimecode() {
this.timecode = await this.pullData("/transports/0/timecode");
BMDCamera.updateUITimecode();
return this.timecode;
}
async getPresets() {
this.pullData("/presets").then((value) => {this.presets = value.presets; BMDCamera.updateUIPresets();});
return this.presets;
}
async getActivePreset() {
this.activePreset = await this.pullData("/presets/active");
BMDCamera.updateUIActivePreset();
return this.activePreset;
}
async getAperture() {
this.pullData("/lens/iris").then((value) => {this.apertureStop = value.apertureStop; this.apertureNormalised = value.normalised; BMDCamera.updateUIAperture();});
return this.apertureNormalised;
}
async getZoom() {
this.pullData("/lens/zoom").then((value) => {this.zoomMM = value.focalLength; this.zoomNormalised = value.normalised; BMDCamera.updateUIZoom();});
return this.zoomNormalised;
}
async getFocus() {
this.pullData("/lens/focus").then((value) => {this.focusNormalised = value.normalised; BMDCamera.updateUIFocus();});
return this.focusNormalised;
}
async getISO() {
this.pullData("/video/iso").then((value) => {this.ISO = value.iso; BMDCamera.updateUIISO();});
return this.ISO;
}
async getGain() {
this.pullData("/video/gain").then((value) => {this.gain = value.gain; BMDCamera.updateUIgain();});
return this.gain;
}
async getWhiteBalance() {
this.pullData("/video/whiteBalance").then((value) => {this.WhiteBalance = value.whiteBalance});
this.pullData("/video/whiteBalanceTint").then((value) => {this.WhiteBalanceTint = value.whiteBalanceTint; BMDCamera.updateUIWhiteBalance();});
return this.WhiteBalance;
}
async getND() {
this.pullData("/video/ndFilter").then((value) => {this.NDStop = value.stop});
this.pullData("/video/ndFilter/displayMode").then((value) => {this.NDMode = value.displayMode; BMDCamera.updateUINDStop();});
return this.NDStop;
}
async getShutter() {
this.shutter = await this.pullData("/video/shutter");
BMDCamera.updateUIshutter();
return this.shutter;
}
async getAutoExposureMode() {
this.AutoExposureMode = await this.pullData("/video/autoExposure");
BMDCamera.updateUIAutoExposureMode();
return this.AutoExposureMode;
}
// This one just fetches the data and stores it in the normal objects. No return value.
async getColorCorrection() {
this.CClift = await this.pullData("/colorCorrection/lift");
this.CCgamma = await this.pullData("/colorCorrection/gamma");
this.CCgain = await this.pullData("/colorCorrection/gain");
this.CCoffset = await this.pullData("/colorCorrection/offset");
this.CCcontrast = await this.pullData("/colorCorrection/contrast");
this.CCcolor = await this.pullData("/colorCorrection/color");
this.CClumacontribution = await this.pullData("/colorCorrection/lumaContribution");
BMDCamera.updateUIColorCorrection();
}
// This method usually takes 200-250 ms
async getAllInfo() {
await this.getFormat();
await this.getTransportMode();
await this.getPlaybackState();
await this.getRecordState();
await this.getTimecode();
await this.getPresets();
await this.getActivePreset();
await this.getAperture();
await this.getZoom();
await this.getFocus();
await this.getISO();
await this.getGain();
await this.getWhiteBalance();
await this.getND();
await this.getShutter();
await this.getAutoExposureMode();
await this.getColorCorrection();
}
// =============== SETTERS ==================
// name, hostname, APIaddress should never have to be set
async setCodecFormat(newCodecFormatObject) {
await this.pushData("/system/codecFormat",newCodecFormatObject);
await sleep(500);
await this.getCodecFormat();
}
async setVideoFormat(newVideoFormatObject) {
await this.pushData("/system/videoFormat",newVideoFormatObject);
await sleep(500);
await this.getCodecFormat();
}
async setTransportMode(newTransportModeString) {
await this.pushData("/transports/0",{"mode": newTransportModeString});
await sleep(500);
await this.getTransportMode();
}
async setPlaybackState(playbackStateObject) {
await this.pushData("/transports/0/playback",playbackStateObject);
await sleep(500);
await this.getPlaybackState();
}
async sendPresetFile(file) {
await sendRequest("POST",this.APIAddress+"/presets",file);
}
async setActivePreset(presetString) {
await this.pushData("/presets/active",{"preset": presetString});
await sleep(500);
await this.getAllInfo();
}
async updatePreset(presetString) {
await this.pushData("/presets/active",{"preset": presetString});
await sleep(500);
await this.getPresets();
}
async setAperture(apertureNormalisedFloat) {
await this.pushData("/lens/iris",{"normalised": apertureNormalisedFloat});
await sleep(1500);
await this.getAperture();
}
async setZoom(zoomNormalisedFloat) {
await this.pushData("/lens/zoom",{"normalised": zoomNormalisedFloat});
await sleep(1500);
await this.getZoom();
}
async setFocus(focusNormalisedFloat) {
await this.pushData("/lens/focus",{"normalised": focusNormalisedFloat});
await sleep(1500);
await this.getFocus();
}
async setISO(ISOint) {
await this.pushData("/video/iso",{"iso":ISOint});
await sleep(500);
await this.getISO();
await this.getGain();
}
async setGain(gainInt) {
await this.pushData("/video/gain",{"gain":gainInt});
await sleep(500);
await this.getGain();
await this.getISO();
}
async setWhiteBalance(whiteBalanceInt, whiteBalanceTintInt) {
await this.pushData("/video/whiteBalance",{"whiteBalance": whiteBalanceInt});
await this.pushData("/video/whiteBalanceTint",{"whiteBalanceTint": whiteBalanceTintInt});
await sleep(500);
await this.getWhiteBalance();
}
async setND(NDstopInt) {
await this.pushData("/video/ndFilter",{"stop": NDstopInt});
await sleep(500);
await this.getND();
}
async setNDDisplayMode(displayModeString) {
await this.pushData("/video/ndFilter/displayMode",{"displayMode": displayModeString});
await sleep(500);
await this.getND();
}
// Accepts JSON obejcts with either shutterSpeed or shutterAngle properties
// Note that the shutterAngle value returned by the API is 100x the actual value
async setShutter(shutterObject) {
await this.pushData("/video/shutter",shutterObject);
await sleep(500);
await this.getShutter();
}
async setAutoExposureMode(AEmodeObject) {
await this.pushData("/video/autoExposure",AEmodeObject);
await sleep(500);
await this.getAutoExposureMode();
}
async setCCLift(CCliftObject) {
await this.pushData("/colorCorrection/lift",CCliftObject);
await sleep(500);
await this.getColorCorrection();
}
async setCCGamma(CCgammaObject) {
await this.pushData("/colorCorrection/gamma",CCgammaObject);
await sleep(500);
await this.getColorCorrection();
}
async setCCGain(CCgainObject) {
await this.pushData("/colorCorrection/gain",CCgainObject);
await sleep(500);
await this.getColorCorrection();
}
async setCCOffset(CCoffsetObject) {
await this.pushData("/colorCorrection/offset",CCoffsetObject);
await sleep(500);
await this.getColorCorrection();
}
async setCCContrast(CCcontrastObject) {
await this.pushData("/colorCorrection/contrast",CCcontrastObject);
await sleep(500);
await this.getColorCorrection();
}
async setCCColor(CCcolorObject) {
await this.pushData("/colorCorrection/color",CCcolorObject);
await sleep(500);
await this.getColorCorrection();
}
async setCCLumaContribuion(CClumacontributionObject) {
await this.pushData("/colorCorrection/lumaContribution",CClumacontributionObject);
await sleep(500);
await this.getColorCorrection();
}
// =============== Other Commands =======================
async doAutoFocus() {
await this.pushData("/lens/focus/doAutoFocus");
sleep(1500).then(() => this.getFocus());
}
async play() {
await this.pushData("/transports/0/play");
await sleep(500);
await this.getPlaybackState();
}
async record() {
await this.pushData("/transports/0/record",{"recording": true});
await sleep(1000);
await this.getRecordState();
}
async stopTransport() {
await this.pushData("/transports/0/stop");
await sleep(1000);
await this.getPlaybackState();
}
async stopRecord() {
await this.pushData("/transports/0/record",{"recording": false});
await sleep(1000);
await this.getRecordState();
}
}
/* Helper Functions */
async function sendRequest(method, url, data) {
const xhttp = new XMLHttpRequest();
var responseObject;
xhttp.onload = function() {
if (this.status == 200) {
// Success w/ Data
responseObject = JSON.parse(this.responseText);
} else {
// Pass along response data and stuff
responseObject = this;
}
}
xhttp.open(method, url, false);
xhttp.send(JSON.stringify(data));
return responseObject;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/* (c) Dylan Speiser 2024
github.com/DylanSpeiser */

272
BMDevice.js Normal file
View file

@ -0,0 +1,272 @@
/* Blackmagic Camera Control JS Class
Written based on the Camera Control
API Documentation from Blackmagic's
Developer info website.
(c) Dylan Speiser 2024
github.com/DylanSpeiser
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published.
This program is distributed in the hope that it will be useful but without
any warranty; without even the implied warranty of merchantability or fitness
for a particular purpose as specified by the License, which you should consult
for more details at LICENSE.txt in this repository.
*/
// Generic Blackmagic Device class
class BMDevice {
// Pretty name and network hostname (strings)
name;
hostname;
APIAddress;
// 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) {
// Set name properties
this.hostname = hostname;
this.APIAddress = "http://"+hostname+"/control/api/v1";
this.name = this.hostname.replace(".local","").replaceAll("-"," ");
// Initialize WebSocket
this.ws = new WebSocket("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);
}
// 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");
}
// Boolean parameter, true = forward, false = backwards
seek(direction) {
let clips = this.GETdata("/timelines/0")?.clips;
let playbackData = this.GETdata("/transports/playback");
let runningSum = 0;
let currentClipIndex = 0;
let clipStartingTimecodes = [];
let i = 0;
clips.forEach((clip) => {
if (runningSum+clip.frameCount > playbackData.position) {
currentClipIndex = i;
}
clipStartingTimecodes[i] = runningSum;
runningSum += clip.frameCount;
i++;
});
let newClipIndex = min(max(0,(direction ? currentClipIndex+1 : currentClipIndex-1)), clips.length);
playbackData.position = clipStartingTimecodes[newClipIndex];
this.PUTdata("/transports/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;
}
this.PUTdata("/transports/0/playback", newStateObj);
}
}
// Child Class Specifically for Cameras
class BMCamera extends BMDevice {
// Child class constructor
// Just passing the hostname to the superclass's constructor
constructor(hostname) {
super(hostname);
}
// 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});
}
}
/* 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
}
};
// 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));
// Return response data
return responseObject;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/* (c) Dylan Speiser 2024
github.com/DylanSpeiser */

View file

@ -3,7 +3,7 @@ This web app is utilizes the [Blackmagic](https://blackmagicdesign.com) Camera C
>This program was written based on the official REST API documentation from Blackmagic, which can be found [here](https://documents.blackmagicdesign.com/DeveloperManuals/RESTAPIforBlackmagicCameras.pdf)
Using this tool, you can control your Blackmagic studio and cinema cameras *without any extra hardware!* Use it for remote monitoring, color correction, focus pulling, or keeping tabs on your eqiupment. The `BMD-Camera-Control.js` file is also useful if you want to write your own web app using the REST API. More details on how to interface with it can be found below.
Using this tool, you can control your Blackmagic studio and cinema cameras *without any extra hardware!* Use it for remote monitoring, color correction, focus pulling, or keeping tabs on your eqiupment. The `BMCamera.js` file is also useful if you want to write your own web app using the REST API. More details on how to interface with it can be found below.
![Screenshot 1](screenshots/WebUI1.png)
@ -74,10 +74,10 @@ If you like this project and want it to improve, consider making a Pull Request
## Tutorials
For more information about using the BMD REST API, check out [my tutorial series](https://github.com/DylanSpeiser/BM-API-Tutorial/) that explains the basics of how to interact with the camera in JavaScript and Python.
## Using `BMD-Camera-Control.js`
## Using `BMCamera.js`
You are more than welcome to use this JavaScript class in your own projects. Just include the file (with its attributions).
<br><br>
Cameras are represented as BMDCamera objects, instantiated with the `new` keyword and the constructor, which takes the hostname as a String argument.
Cameras are represented as BMCamera objects, instantiated with the `new` keyword and the constructor, which takes the hostname as a String argument.
<br><br>
After instantiation, the constructor does NOT automatically fetch any data from the camera. This can be done using the `getAllInfo()` method, or the individual getters if you only need a few details. These are all asynchronous and so must be waited upon using `await` or `.then()`. Consult your nearest Google search bar for help implementing asynchronous JavaScript, that's how I did it.
<br><br>
@ -86,7 +86,7 @@ Many of setter functions take Objects as arguments, rather than Strings or ints.
This file is heavily commented so everything _should_ be pretty clear, but let me know in the Issue tracker if you're having trouble.
## BYOUI
If you want to use `BMD-Camera-Control.js` in your own UI, there are static references to functions in the BMDCamera class that get called after changing a value. They all look like `updateUIxxxxxx()`. Set these references to point to your UI updating routines in _your_ source file. No need to write them in `BMD-Camera-Control.js`.
If you want to use `BMCamera.js` in your own UI, there are static references to functions in the BMCamera class that get called after changing a value. They all look like `updateUIxxxxxx()`. Set these references to point to your UI updating routines in _your_ source file. No need to write them in `BMCamera.js`.
# Issues and To-Dos
## Known Issues

View file

@ -14,7 +14,7 @@
</head>
<body onload="bodyOnLoad()">
<!-- JavaScript Linking -->
<script src="BMD-Camera-Control.js"></script>
<script src="BMDevice.js"></script>
<script src="web-ui.js"></script>
<!------ Page Content ------>
@ -52,57 +52,57 @@
<div class="flexContainerH" id="cameraControlColorCorrectionContainer">
<!-- <div class="flexContainerH" id="cameraControlLGGTabs">
<a href="#" class="ccTabLabel selectedTab" onclick="setCCMode(0)">Lift</a>
<a href="#" class="ccTabLabel" onclick="setCCMode(1)">Gamma</a>
<a href="#" class="ccTabLabel" onclick="setCCMode(2)">Gain</a>
<a href="#" class="ccTabLabel selectedTab" onclick="">Lift</a>
<a href="#" class="ccTabLabel" onclick="">Gamma</a>
<a href="#" class="ccTabLabel" onclick="">Gain</a>
</div> -->
<span style="margin-top: 0.5em;">Lift</span>
<div class="flexContainerH" id="cameraControlColorCorrectionBottomContainer">
<button class="CCResetButton circleButton" onclick="resetCC(0)" title="Reset Lift">&#10227</button>
<button class="CCResetButton circleButton" onclick="" title="Reset Lift">&#10227</button>
<div class="flexContainerH" id="cameraControlColorCorrectionNumbersContainer">
<span contenteditable="plaintext-only" style="text-decoration-color: #dbdbdb;" class="CClumaLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(0)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #e64b3d;" class="CCredLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(0)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #00a841;" class="CCgreenLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(0)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #2a78c8;" class="CCblueLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(0)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #dbdbdb;" class="CClumaLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #e64b3d;" class="CCredLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #00a841;" class="CCgreenLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #2a78c8;" class="CCblueLabel" onkeydown="">0.00</span>
</div>
<button id="CCHamburgerButton" class="circleButton" onclick="setCCFromUI(0)" title="Set Lift">&#10138</button>
<button id="CCHamburgerButton" class="circleButton" onclick="" title="Set Lift">&#10138</button>
</div>
<span>Gamma</span>
<div class="flexContainerH" id="cameraControlColorCorrectionBottomContainer">
<button class="CCResetButton circleButton" onclick="resetCC(1)" title="Reset Gamma">&#10227</button>
<button class="CCResetButton circleButton" onclick="" title="Reset Gamma">&#10227</button>
<div class="flexContainerH" id="cameraControlColorCorrectionNumbersContainer">
<span contenteditable="plaintext-only" style="text-decoration-color: #dbdbdb;" class="CClumaLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(1)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #e64b3d;" class="CCredLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(1)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #00a841;" class="CCgreenLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(1)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #2a78c8;" class="CCblueLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(1)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #dbdbdb;" class="CClumaLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #e64b3d;" class="CCredLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #00a841;" class="CCgreenLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #2a78c8;" class="CCblueLabel" onkeydown="">0.00</span>
</div>
<button id="CCHamburgerButton" class="circleButton" onclick="setCCFromUI(1)" title="Set Gamma">&#10138</button>
<button id="CCHamburgerButton" class="circleButton" onclick="" title="Set Gamma">&#10138</button>
</div>
<span>Gain</span>
<div class="flexContainerH" id="cameraControlColorCorrectionBottomContainer">
<button class="CCResetButton circleButton" onclick="resetCC(2)" title="Reset Gain">&#10227</button>
<button class="CCResetButton circleButton" onclick="" title="Reset Gain">&#10227</button>
<div class="flexContainerH" id="cameraControlColorCorrectionNumbersContainer">
<span contenteditable="plaintext-only" style="text-decoration-color: #dbdbdb;" class="CClumaLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(2)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #e64b3d;" class="CCredLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(2)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #00a841;" class="CCgreenLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(2)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #2a78c8;" class="CCblueLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(2)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #dbdbdb;" class="CClumaLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #e64b3d;" class="CCredLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #00a841;" class="CCgreenLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #2a78c8;" class="CCblueLabel" onkeydown="">0.00</span>
</div>
<button id="CCHamburgerButton" class="circleButton" onclick="setCCFromUI(2)" title="Set Gain">&#10138</button>
<button id="CCHamburgerButton" class="circleButton" onclick="" title="Set Gain">&#10138</button>
</div>
<span>Offset</span>
<div class="flexContainerH" id="cameraControlColorCorrectionBottomContainer">
<button class="CCResetButton circleButton" onclick="resetCC(3)" title="Reset Offset">&#10227</button>
<button class="CCResetButton circleButton" onclick="" title="Reset Offset">&#10227</button>
<div class="flexContainerH" id="cameraControlColorCorrectionNumbersContainer">
<span contenteditable="plaintext-only" style="text-decoration-color: #dbdbdb;" class="CClumaLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(3)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #e64b3d;" class="CCredLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(3)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #00a841;" class="CCgreenLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(3)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #2a78c8;" class="CCblueLabel" onkeydown="if (event.key === 'Enter') {event.preventDefault(); setCCFromUI(3)}">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #dbdbdb;" class="CClumaLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #e64b3d;" class="CCredLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #00a841;" class="CCgreenLabel" onkeydown="">0.00</span>
<span contenteditable="plaintext-only" style="text-decoration-color: #2a78c8;" class="CCblueLabel" onkeydown="">0.00</span>
</div>
<button id="CCHamburgerButton" class="circleButton" onclick="setCCFromUI(3)" title="Set Offset">&#10138</button>
<button id="CCHamburgerButton" class="circleButton" onclick="" title="Set Offset">&#10138</button>
</div>
</div>
@ -110,38 +110,38 @@
<div class="ccExposureSettingContainer">
<span class="exposureControlLabel">FILTER</span>
<div class="ccExposureSettingValueContainer">
<a class="expAdjArr" href="#" onclick="decreaseND()" id="NDL">&#9664</a>
<span id="ndFilterSpan" contenteditable="plaintext-only" onkeydown="if (event.key === 'Enter') {event.preventDefault(); cameras[ci].setND(parseInt(this.innerHTML))}">0</span>
<a class="expAdjArr" href="#" onclick="increaseND()" id="NDR">&#9654</a>
<a class="expAdjArr" href="#" onclick="" id="NDL">&#9664</a>
<span id="ndFilterSpan" contenteditable="plaintext-only" onkeydown="">0</span>
<a class="expAdjArr" href="#" onclick="" id="NDR">&#9654</a>
</div>
</div>
<div class="ccExposureSettingContainer">
<span class="exposureControlLabel">GAIN</span>
<div class="ccExposureSettingValueContainer">
<a class="expAdjArr" href="#" onclick="decreaseGain()" id="GAL">&#9664</a>
<span id="gainSpan" contenteditable="plaintext-only" onkeydown="if (event.key === 'Enter') {event.preventDefault(); cameras[ci].setGain(parseInt(this.innerHTML))}">+0db</span>
<a class="expAdjArr" href="#" onclick="increaseGain()" id="GAR">&#9654</a>
<a class="expAdjArr" href="#" onclick="" id="GAL">&#9664</a>
<span id="gainSpan" contenteditable="plaintext-only" onkeydown="">+0db</span>
<a class="expAdjArr" href="#" onclick="" id="GAR">&#9654</a>
</div>
</div>
<div class="ccExposureSettingContainer">
<span class="exposureControlLabel">SHUTTER</span>
<div class="ccExposureSettingValueContainer">
<a class="expAdjArr" href="#" onclick="decreaseShutter()" id="SHL">&#9664</a>
<span id="shutterSpan" contenteditable="plaintext-only" onkeydown="if (event.key === 'Enter') {event.preventDefault(); handleShutterInput(this.innerHTML)}">1/50</span>
<a class="expAdjArr" href="#" onclick="increaseShutter()" id="SHR">&#9654</a>
<a class="expAdjArr" href="#" onclick="" id="SHL">&#9664</a>
<span id="shutterSpan" contenteditable="plaintext-only" onkeydown="">1/50</span>
<a class="expAdjArr" href="#" onclick="" id="SHR">&#9654</a>
</div>
</div>
<div class="ccExposureSettingContainer">
<span class="exposureControlLabel" onclick="swapWBMode()" title="Click here to swap between WB and Tint" id="WBLabel">BALANCE</span>
<div class="ccExposureSettingValueContainer" id="WBValueContainer">
<a class="expAdjArr" href="#" onclick="decreaseWhiteBalance()" id="WBL">&#9664</a>
<span id="whiteBalanceSpan" contenteditable="plaintext-only" onkeydown="if (event.key === 'Enter') {event.preventDefault(); cameras[ci].setWhiteBalance(parseInt(this.innerHTML),cameras[ci].whiteBalanceTint)}">5600K</span>
<a class="expAdjArr" href="#" onclick="increaseWhiteBalance()" id="WBR">&#9654</a>
<a class="expAdjArr" href="#" onclick="" id="WBL">&#9664</a>
<span id="whiteBalanceSpan" contenteditable="plaintext-only" onkeydown="">5600K</span>
<a class="expAdjArr" href="#" onclick="" id="WBR">&#9654</a>
</div>
<div class="ccExposureSettingValueContainer dNone" id="WBTintValueContainer">
<a class="expAdjArr" href="#" onclick="decreaseWhiteBalanceTint()" id="WBTL">&#9664</a>
<span id="whiteBalanceTintSpan" contenteditable="plaintext-only" onkeydown="if (event.key === 'Enter') {event.preventDefault(); cameras[ci].setWhiteBalance(cameras[ci].whiteBalance,parseInt(this.innerHTML))}">0</span>
<a class="expAdjArr" href="#" onclick="increaseWhiteBalanceTint()" id="WBLR">&#9654</a>
<a class="expAdjArr" href="#" onclick="" id="WBTL">&#9664</a>
<span id="whiteBalanceTintSpan" contenteditable="plaintext-only" onkeydown="">0</span>
<a class="expAdjArr" href="#" onclick="" id="WBLR">&#9654</a>
</div>
</div>
</div>
@ -149,17 +149,17 @@
<div class="flexContainerH" id="cameraControlLensContainer">
<div class="lensSliderContainer">
<span>FOCUS</span>
<input type="range" orient="vertical" max="1" min="0" step="0.001" id="focusRange" onmouseup="cameras[ci].setFocus(parseFloat(this.value))">
<button id="AFButton" class="circleButton" onclick="cameras[ci].doAutoFocus()">AF</button>
<input type="range" orient="vertical" max="1" min="0" step="0.001" id="focusRange" onmouseup="">
<button id="AFButton" class="circleButton" onclick="">AF</button>
</div>
<div class="lensSliderContainer">
<span>IRIS</span>
<input type="range" orient="vertical" max="1" min="0" step="0.001" id="irisRange" onmouseup="cameras[ci].setAperture(parseFloat(this.value))">
<input type="range" orient="vertical" max="1" min="0" step="0.001" id="irisRange" onmouseup="">
<span id="apertureStopsLabel">X.X</span>
</div>
<div class="lensSliderContainer">
<span>ZOOM</span>
<input type="range" orient="vertical" max="1" min="0" step="0.001" id="zoomRange" onmouseup="cameras[ci].setZoom(parseFloat(this.value))">
<input type="range" orient="vertical" max="1" min="0" step="0.001" id="zoomRange" onmouseup="">
<span id="zoomMMLabel">XXmm</span>
</div>
</div>
@ -176,13 +176,13 @@
</div>
<div id="transportControls">
<!-- These will be sticky buttons for loop, single clip, record -->
<!--<button class="circleButton" onclick="" title="Loop">&#8635</button>
<button class="circleButton" onclick="" title="Loop">&#8635</button>
<button class="circleButton" onclick="" title="Single Clip">S</button>
<button class="circleButton" onclick="" title="Back">&#9204</button>
<button class="circleButton" onclick="" title="Forward">&#9205</button> -->
<button class="circleButton" onclick="cameras[ci].record()" title="Record">&#9210</button>
<!-- <button class="circleButton" onclick="cameras[ci].play()" title="Play">&#9654</button> -->
<button class="circleButton" onclick="cameras[ci].stopTransport(); cameras[ci].stopRecord();" title="Stop">&#9209</button>
<button class="circleButton" onclick="" title="Forward">&#9205</button>
<button class="circleButton" onclick="cameras[ci].toggleRecord()" title="Record">&#9210</button>
<button class="circleButton" onclick="cameras[ci].play()" title="Play">&#9654</button>
<button class="circleButton" onclick="cameras[ci].stop()" title="Stop">&#9209</button>
</div>
<h2 id="timecodeLabel">TIMECODE</h2>
</div>
@ -195,7 +195,7 @@
<td>Hostname</td>
<td>
<input type="text" placeholder="Studio-Camera-6K-Pro.local" id="hostnameInput">
<button onclick='initCamera(document.getElementById("hostnameInput").value)'>Connect</button>
<button onclick="initCamera()">Connect</button>
<span id="connectionErrorSpan"></span>
</td>
</tr>
@ -227,12 +227,12 @@
<tr>
<td>Preset Select</td>
<td>
<select id="presetsDropDown" onchange="cameras[ci].setActivePreset(this.value+'.cset')">
<select id="presetsDropDown" onchange="">
<!-- Auto-populated by updateUIPresets() -->
</select>
</td>
<td>
<button onclick="cameras[ci].setActivePreset(document.getElementById('presetsDropDown').value+'.cset')">Restore from Preset</button>
<button onclick="">Restore from Preset</button>
</td>
</tr>
</table>
@ -243,7 +243,7 @@
<table>
<tr>
<td>ISO</td>
<td><input type="number" id="ISOInput" step="100" onkeydown="if (event.key === 'Enter') {event.preventDefault(); cameras[ci].setISO(parseInt(this.value))}"></td>
<td><input type="number" id="ISOInput" step="100" onkeydown=""></td>
</tr>
<tr>
<td>AE Mode</td>
@ -268,7 +268,7 @@
</td>
</tr>
</table>
<button style="margin: 2vh 0 0 3.5vw;" onclick="AEmodeInputHandler()">Set AE Mode</button>
<button style="margin: 2vh 0 0 3.5vw;" onclick="">Set AE Mode</button>
</div>
<div class="tableControl">
@ -276,19 +276,19 @@
<table>
<tr>
<td>Pivot</td>
<td><input type="range" max="5" min="-5" step="0.001" id="CCcontrastPivotRange" onmouseup="cameras[ci].setCCContrast({'pivot': parseFloat(this.value)})"></td>
<td><input type="range" max="0" min="1" step="0.001" id="CCcontrastPivotRange" onmouseup=""></td>
<td>
<span id="CCcontrastPivotLabel" contenteditable="plaintext-only" onkeydown="if (event.key === 'Enter') {event.preventDefault(); cameras[ci].setCCContrast({'pivot': parseFloat(this.innerHTML)})}">0</span>
<span id="CCcontrastPivotLabel" contenteditable="plaintext-only" onkeydown="">0</span>
</td>
<td rowspan="2">
<button class="CCResetButton circleButton" onclick="resetCC(4)" title="Reset Contrast">&#10227</button>
<button class="CCResetButton circleButton" onclick="" title="Reset Contrast">&#10227</button>
</td>
</tr>
<tr>
<td>Adjust</td>
<td><input type="range" max="5" min="-5" step="0.001" id="CCcontrastAdjustRange" onmouseup="cameras[ci].setCCContrast({'adjust': parseFloat(this.value)})"></td>
<td><input type="range" max="0" min="2" step="0.001" id="CCcontrastAdjustRange" onmouseup=""></td>
<td>
<span id="CCcontrastAdjustLabel" contenteditable="plaintext-only" onkeydown="if (event.key === 'Enter') {event.preventDefault(); cameras[ci].setCCContrast({'adjust': parseFloat(this.innerHTML)})}">0</span>
<span id="CCcontrastAdjustLabel" contenteditable="plaintext-only" onkeydown="">0</span>
</td>
</tr>
</table>
@ -299,26 +299,26 @@
<table>
<tr>
<td>Hue</td>
<td><input type="range" max="1" min="-1" step="0.001" id="CChueRange" onmouseup="cameras[ci].setCCColor({'hue': parseFloat(this.value)})"></td>
<td><input type="range" max="1" min="-1" step="0.001" id="CChueRange" onmouseup=""></td>
<td>
<span id="CCcolorHueLabel" contenteditable="plaintext-only" onkeydown="if (event.key === 'Enter') {event.preventDefault(); cameras[ci].setCCColor({'hue': parseFloat(this.innerHTML)})}">0</span>
<span id="CCcolorHueLabel" contenteditable="plaintext-only" onkeydown="">0</span>
</td>
<td rowspan="3">
<button class="CCResetButton circleButton" onclick="resetCC(5)" title="Reset Color">&#10227</button>
<button class="CCResetButton circleButton" onclick="" title="Reset Color">&#10227</button>
</td>
</tr>
<tr>
<td>Saturation</td>
<td><input type="range" max="2" min="0" step="0.001" id="CCsaturationRange" onmouseup="cameras[ci].setCCColor({'saturation': parseFloat(this.value)})"></td>
<td><input type="range" max="2" min="0" step="0.001" id="CCsaturationRange" onmouseup=""></td>
<td>
<span id="CCcolorSatLabel" contenteditable="plaintext-only" onkeydown="if (event.key === 'Enter') {event.preventDefault(); cameras[ci].setCCColor({'saturation': parseFloat(this.innerHTML)})}">0</span>
<span id="CCcolorSatLabel" contenteditable="plaintext-only" onkeydown="">0</span>
</td>
</tr>
<tr>
<td>Luma Contribution</td>
<td><input type="range" max="1" min="0" step="0.001" id="CClumaContributionRange" onmouseup="cameras[ci].setCCLumaContribuion({'lumaContribution': parseFloat(this.value)})"></td>
<td><input type="range" max="1" min="0" step="0.001" id="CClumaContributionRange" onmouseup=""></td>
<td>
<span id="CCcolorLCLabel" contenteditable="plaintext-only" onkeydown="if (event.key === 'Enter') {event.preventDefault(); cameras[ci].setCCLumaContribuion({'lumaContribution': parseFloat(this.innerHTML)})}">0</span>
<span id="CCcolorLCLabel" contenteditable="plaintext-only" onkeydown="">0</span>
</td>
</tr>
</table>
@ -331,14 +331,12 @@
<!-- Footer Div -->
<div class="flexContainerH" id="footerContainer">
<div id="footerLeft">
<button onclick="refresh();">Refresh</button>
<span id="refreshingText" class="refreshing">Refreshing...</span>
<span class="">(v 1.1)</span>
</div>
<div id="footerLinks">
<span><a id="documentationLink" href="#" target="_blank">YAML Documentation</a></span>
<span><a id="mediaManagerLink" href="#" target="_blank">Web Media Manager</a></span>
<span><a id="githubLink" href="#" target="https://github.com/DylanSpeiser/BM-Camera-Control-WebUI">GitHub</a></span>
<span class="">(v 1.0)</span>
</div>
</div>
</body>

View file

@ -219,6 +219,10 @@ input[type=file]:focus {
margin-right: 1.25vw;
}
#footerLeft span {
margin-left: 1.25vw;
}
#footerLinks a {
text-decoration: none;
color: #e66c01;

450
web-ui.js
View file

@ -12,142 +12,86 @@ var ci = 0; // Index into this array for the currently selected came
var WBMode = 0; // 0: balance, 1: tint
var defaultControlsHTML;
// Set everything up
function bodyOnLoad() {
// Initialize timers
let intervalIDOne = setInterval(timerCallFunction1, 1000); // One second timer for refreshing record / timecode
let intervalIDTen = setInterval(timerCallFunction10, 10000); // Ten second timer for refreshing everything
// Pass along the UI refreshing methods to the BMDCamera class
BMDCamera.updateUIAll = updateUIAll;
BMDCamera.updateUIname = updateUIname;
BMDCamera.updateUIhostname = updateUIhostname;
BMDCamera.updateUIFormat = updateUIFormat;
BMDCamera.updateUITransportMode = updateUITransportMode;
BMDCamera.updateUIPlaybackState = updateUIPlaybackState;
BMDCamera.updateUIRecordState = updateUIRecordState;
BMDCamera.updateUITimecode = updateUITimecode;
BMDCamera.updateUIPresets = updateUIPresets;
BMDCamera.updateUIActivePreset = updateUIActivePreset;
BMDCamera.updateUIAperture = updateUIAperture;
BMDCamera.updateUIZoom = updateUIZoom;
BMDCamera.updateUIFocus = updateUIFocus;
BMDCamera.updateUIISO = updateUIISO;
BMDCamera.updateUIgain = updateUIgain;
BMDCamera.updateUIWhiteBalance = updateUIWhiteBalance;
BMDCamera.updateUINDStop = updateUINDStop;
BMDCamera.updateUIshutter = updateUIshutter;
BMDCamera.updateUIAutoExposureMode = updateUIAutoExposureMode;
BMDCamera.updateUIColorCorrection = updateUIColorCorrection;
BMDCamera.updateUILinks = updateUILinks;
defaultControlsHTML = document.getElementById("allCamerasContainer").innerHTML;
}
// Basically a wrapper for BMDCamera() constructor
// Checks the hostname, if it replies successfully then a new BMDCamera object
// Checks the hostname, if it replies successfully then a new BMCamera object
// is made and gets put in the array at ind
function initCamera(hostname) {
// Check if the hostname is valid
sendRequest("GET", "http://"+hostname+"/control/api/v1/system","").then((response) => {
function initCamera() {
// Get hostname from Hostname text field
let hostname = document.getElementById("hostnameInput").value;
try {
// Check if the hostname is valid
let response = sendRequest("GET", "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 BMDCamera(hostname);
cameras[ci].getAllInfo();
cameras[ci] = new BMCamera(hostname);
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 => {
} 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)";
});
}
}
// Important refreshing function. Gets all of the camera's info and populates the UI with it.
// This function controls the visibility of the "Refreshing..." text in the bottom right corner.
function refresh() {
document.getElementById("refreshingText").classList.add("refreshing");
cameras[ci].getAllInfo().then(() => {
document.getElementById("refreshingText").classList.remove("refreshing");
}
);
}
// =============================== UI Updaters ==================================
// ==============================================================================
// =============================== UI Updater ==================================
// =============================================================================
function updateUIAll() {
updateUIname();
updateUIhostname();
updateUIFormat();
updateUITransportMode();
updateUIPlaybackState();
updateUIRecordState();
updateUITimecode();
updateUIPresets();
updateUIActivePreset();
updateUIAperture();
updateUIZoom();
updateUIFocus();
updateUIISO();
updateUIgain();
updateUIWhiteBalance();
updateUINDStop();
updateUIshutter();
updateUIAutoExposureMode();
updateUIColorCorrection();
updateUILinks();
}
// ========== Camera Name ==========
function updateUIname() {
document.getElementById("cameraName").innerHTML = cameras[ci].name;
}
function updateUIhostname() {
// ========== Hostname ==========
document.getElementById("hostnameInput").value = cameras[ci].hostname;
}
function updateUIFormat() {
document.getElementById("formatCodec").innerHTML = cameras[ci].format.codec.toUpperCase().replace(":"," ").replace("_",":");
// ========== Format ==========
document.getElementById("formatCodec").innerHTML = cameras[ci].propertyData['/system/format']?.codec.toUpperCase().replace(":"," ").replace("_",":");
let resObj = cameras[ci].format.recordResolution;
document.getElementById("formatResolution").innerHTML = resObj.width + "x" + resObj.height;
document.getElementById("formatFPS").innerHTML = cameras[ci].format.frameRate+" fps";
}
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 updateUITransportMode() {
//TBI
}
// ========== Recording State ==========
function updateUIPlaybackState() {
//TBI
}
function updateUIRecordState() {
if (cameras[ci].recordState.recording) {
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 updateUITimecode() {
var tcString = parseInt(cameras[ci].timecode.timecode.toString(16),10).toString().padStart(8,'0').match(/.{1,2}/g).join(':');
// ========== Timecode ==========
document.getElementById("timecodeLabel").innerHTML = tcString;
}
document.getElementById("timecodeLabel").innerHTML = parseTimecode(cameras[ci].propertyData['/transports/0/timecode']?.timecode);
function updateUIPresets() {
// ========== Presets Dropdown ==========
var presetsList = document.getElementById("presetsDropDown");
presetsList.innerHTML = "";
cameras[ci].presets.forEach((presetItem) => {
cameras[ci].propertyData['/presets']?.presets.forEach((presetItem) => {
let presetName = presetItem.split('.', 1);
let textNode = document.createTextNode(presetName);
@ -156,70 +100,74 @@ function updateUIPresets() {
optionNode.appendChild(textNode);
document.getElementById("presetsDropDown").appendChild(optionNode);
});
}
function updateUIActivePreset() {
// ========== Active Preset ==========
var presetsList = document.getElementById("presetsDropDown");
presetsList.childNodes.forEach((child) => {
if (child.nodeName == 'OPTION' && child.value == cameras[ci].activePreset) {
if (child.nodeName == 'OPTION' && child.value == cameras[ci].propertyData['/presets/active']?.preset) {
child.selected=true
} else {
child.selected=false
}
})
}
function updateUIAperture() {
document.getElementById("irisRange").value = cameras[ci].apertureNormalised;
document.getElementById("apertureStopsLabel").innerHTML = cameras[ci].apertureStop.toFixed(1);
}
// ========== Iris ==========
function updateUIZoom() {
document.getElementById("zoomRange").value = cameras[ci].zoomNormalised;
document.getElementById("zoomMMLabel").innerHTML = cameras[ci].zoomMM +"mm";
}
document.getElementById("irisRange").value = cameras[ci].propertyData['/lens/iris']?.normalised;
document.getElementById("apertureStopsLabel").innerHTML = cameras[ci].propertyData['/lens/iris']?.apertureStop.toFixed(1);
function updateUIFocus() {
document.getElementById("focusRange").value = cameras[ci].focusNormalised;
}
// ========== Zoom ==========
function updateUIISO() {
document.getElementById("ISOInput").value = cameras[ci].ISO;
}
document.getElementById("zoomRange").value = cameras[ci].propertyData['/lens/zoom']?.normalised;
document.getElementById("zoomMMLabel").innerHTML = cameras[ci].propertyData['/lens/zoom']?.focalLength +"mm";
function updateUIgain() {
var gainString = "";
// ========== Focus ==========
if (cameras[ci].gain >= 0) {
gainString = "+"+cameras[ci].gain+"db"
document.getElementById("focusRange").value = cameras[ci].propertyData['/lens/focus']?.normalised;
// ========== ISO ==========
if (cameras[ci].propertyData['/video/iso'])
document.getElementById("ISOInput").value = cameras[ci].propertyData['/video/iso']?.iso;
// ========== GAIN ==========
let gainString = "";
let gainInt = cameras[ci].propertyData['/video/gain']?.gain
if (gainInt >= 0) {
gainString = "+"+gainInt+"db"
} else {
gainString = cameras[ci].gain+"db"
gainString = gainInt+"db"
}
document.getElementById("gainSpan").innerHTML = gainString;
}
function updateUIWhiteBalance() {
document.getElementById("whiteBalanceSpan").innerHTML = cameras[ci].WhiteBalance+"K";
document.getElementById("whiteBalanceTintSpan").innerHTML = cameras[ci].WhiteBalanceTint;
}
// ========== WHITE BALANCE ===========
function updateUINDStop() {
document.getElementById("ndFilterSpan").innerHTML = cameras[ci].NDStop;
if (cameras[ci].UnimplementedFunctionality.includes("/video/ndFilter")) {
document.getElementById("whiteBalanceSpan").innerHTML = cameras[ci].propertyData['/video/whiteBalance']?.whiteBalance+"K";
document.getElementById("whiteBalanceTintSpan").innerHTML = cameras[ci].propertyData['/video/whiteBalanceTint']?.whiteBalanceTint;
// =========== 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;
}
}
function updateUIshutter() {
var shutterString = ""
// ============ Shutter =====================
if ('shutterSpeed' in cameras[ci].shutter) {
shutterString = "1/"+cameras[ci].shutter.shutterSpeed
} else {
var shangleString = (cameras[ci].shutter.shutterAngle / 100).toFixed(1).toString()
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 {
@ -228,59 +176,65 @@ function updateUIshutter() {
}
document.getElementById("shutterSpan").innerHTML = shutterString;
}
function updateUIAutoExposureMode() {
// =========== Auto Exposure Mode ===========
let AEmodeSelect = document.getElementById("AEmodeDropDown");
let AEtypeSelect = document.getElementById("AEtypeDropDown");
AEmodeSelect.value = cameras[ci].AutoExposureMode.mode;
AEtypeSelect.value = cameras[ci].AutoExposureMode.type;
}
AEmodeSelect.value = cameras[ci].propertyData['/video/autoExposure']?.mode;
AEtypeSelect.value = cameras[ci].propertyData['/video/autoExposure']?.type;
// =========== COLOR CORRECTION =============
function updateUIColorCorrection() {
// Lift
document.getElementsByClassName("CClumaLabel")[0].innerHTML = cameras[ci].CClift.luma.toFixed(2);
document.getElementsByClassName("CCredLabel")[0].innerHTML = cameras[ci].CClift.red.toFixed(2);
document.getElementsByClassName("CCgreenLabel")[0].innerHTML = cameras[ci].CClift.green.toFixed(2);
document.getElementsByClassName("CCblueLabel")[0].innerHTML = cameras[ci].CClift.blue.toFixed(2);
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
document.getElementsByClassName("CClumaLabel")[1].innerHTML = cameras[ci].CCgamma.luma.toFixed(2);
document.getElementsByClassName("CCredLabel")[1].innerHTML = cameras[ci].CCgamma.red.toFixed(2);
document.getElementsByClassName("CCgreenLabel")[1].innerHTML = cameras[ci].CCgamma.green.toFixed(2);
document.getElementsByClassName("CCblueLabel")[1].innerHTML = cameras[ci].CCgamma.blue.toFixed(2);
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
document.getElementsByClassName("CClumaLabel")[2].innerHTML = cameras[ci].CCgain.luma.toFixed(2);
document.getElementsByClassName("CCredLabel")[2].innerHTML = cameras[ci].CCgain.red.toFixed(2);
document.getElementsByClassName("CCgreenLabel")[2].innerHTML = cameras[ci].CCgain.green.toFixed(2);
document.getElementsByClassName("CCblueLabel")[2].innerHTML = cameras[ci].CCgain.blue.toFixed(2);
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
document.getElementsByClassName("CClumaLabel")[3].innerHTML = cameras[ci].CCoffset.luma.toFixed(2);
document.getElementsByClassName("CCredLabel")[3].innerHTML = cameras[ci].CCoffset.red.toFixed(2);
document.getElementsByClassName("CCgreenLabel")[3].innerHTML = cameras[ci].CCoffset.green.toFixed(2);
document.getElementsByClassName("CCblueLabel")[3].innerHTML = cameras[ci].CCoffset.blue.toFixed(2);
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
document.getElementById("CCcontrastPivotRange").value = cameras[ci].CCcontrast.pivot;
document.getElementById("CCcontrastPivotLabel").innerHTML = cameras[ci].CCcontrast.pivot.toFixed(2);
document.getElementById("CCcontrastAdjustRange").value = cameras[ci].CCcontrast.adjust;
document.getElementById("CCcontrastAdjustLabel").innerHTML = cameras[ci].CCcontrast.adjust.toFixed(2);
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
document.getElementById("CChueRange").value = cameras[ci].CCcolor.hue;
document.getElementById("CCcolorHueLabel").innerHTML = cameras[ci].CCcolor.hue.toFixed(2);
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 = cameras[ci].CCcolor.saturation;
document.getElementById("CCcolorSatLabel").innerHTML = cameras[ci].CCcolor.saturation.toFixed(2);
document.getElementById("CCsaturationRange").value = colorProps?.saturation;
document.getElementById("CCcolorSatLabel").innerHTML = parseInt(colorProps?.saturation * 50)+"%";
document.getElementById("CClumaContributionRange").value = cameras[ci].CClumacontribution.lumaContribution;
document.getElementById("CCcolorLCLabel").innerHTML = cameras[ci].CClumacontribution.lumaContribution.toFixed(2);
}
let lumaContributionProps = cameras[ci].propertyData['/colorCorrection/lumaContribution'];
document.getElementById("CClumaContributionRange").value = lumaContributionProps?.lumaContribution;
document.getElementById("CCcolorLCLabel").innerHTML = parseInt(lumaContributionProps?.lumaContribution * 100)+"%";
function updateUILinks() {
// ============ Footer Links ===============
document.getElementById("documentationLink").href = "http://"+cameras[ci].hostname+"/control/documentation.html";
document.getElementById("mediaManagerLink").href = "http://"+cameras[ci].hostname;
}
@ -288,32 +242,18 @@ function updateUILinks() {
// ==============================================================================
// One Second Timer Call
// Only updates rec/play state and timecode
function timerCallFunction1() {
if (cameras[ci]) {
cameras[ci].getRecordState();
cameras[ci].getPlaybackState();
cameras[ci].getTimecode();
}
}
// Ten Second Timer Call
// Refreshes all elements
function timerCallFunction10() {
if (cameras[ci]) {
refresh();
}
}
// 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]) {
refresh();
}
// Reset the Controls
document.getElementById("allCamerasContainer").innerHTML = defaultControlsHTML;
// Update the UI
for (var i = 0; i < 8; i++) {
if (i == ci) {
@ -325,6 +265,10 @@ function switchCamera(index) {
document.getElementById("cameraNumberLabel").innerHTML = "CAM"+(ci+1);
document.getElementById("cameraName").innerHTML = "CAMERA NAME";
if (cameras[ci]) {
cameras[ci].active = true;
}
}
// For not-yet-implemented Color Correction UI
@ -384,128 +328,16 @@ function manualAPICall() {
const requestMethod = (requestRadioGET.checked ? "GET" : "PUT");
const requestURL = cameras[ci].APIAddress+requestEndpointText;
sendRequest(requestMethod,requestURL,requestData).then((response) => {
// console.log("Manual API Call Response: ", response);
if (!response.status) {
document.getElementById("manualRequestResponseP").innerHTML = JSON.stringify(response);
} else {
document.getElementById("manualRequestResponseP").innerHTML = response.status+": "+response.statusText;
}
});
}
/* Control Calling Functions */
/* Makes the HTML cleaner. */
function decreaseND() {
cameras[ci].setND(cameras[ci].NDStop-2);
}
function increaseND() {
cameras[ci].setND(cameras[ci].NDStop+2);
}
function decreaseGain() {
cameras[ci].setGain(cameras[ci].gain-2);
}
function increaseGain() {
cameras[ci].setGain(cameras[ci].gain+2);
}
function decreaseShutter() {
let cam = cameras[ci];
if ('shutterSpeed' in cam.shutter) {
cam.setShutter({"shutterSpeed":cam.shutter.shutterSpeed+10});
} else {
cam.setShutter({"shutterAngle": cam.shutter.shutterAngle-1000});
}
}
function increaseShutter() {
let cam = cameras[ci];
if ('shutterSpeed' in cam.shutter) {
cam.setShutter({"shutterSpeed":cam.shutter.shutterSpeed-10});
} else {
cam.setShutter({"shutterAngle": cam.shutter.shutterAngle+1000});
}
}
function handleShutterInput(inputString) {
let cam = cameras[ci];
if ('shutterSpeed' in cam.shutter) {
if (inputString.indexOf("1/") >= 0) {
cam.setShutter({"shutterSpeed" :parseInt(inputString.substring(2))});
} else {
cam.setShutter({"shutterSpeed" :parseInt(inputString)});
}
} else {
cam.setShutter({"shutterAngle": parseInt(parseFloat(inputString)*100)});
}
}
function decreaseWhiteBalance() {
cameras[ci].setWhiteBalance(cameras[ci].WhiteBalance-50,cameras[ci].WhiteBalanceTint);
}
function increaseWhiteBalance() {
cameras[ci].setWhiteBalance(cameras[ci].WhiteBalance+50,cameras[ci].WhiteBalanceTint);
}
function decreaseWhiteBalanceTint() {
cameras[ci].setWhiteBalance(cameras[ci].WhiteBalance,cameras[ci].WhiteBalanceTint-1);
}
function increaseWhiteBalanceTint() {
cameras[ci].setWhiteBalance(cameras[ci].WhiteBalance,cameras[ci].WhiteBalanceTint+1);
}
function AEmodeInputHandler() {
let AEmode = document.getElementById("AEmodeDropDown").value;
let AEtype = document.getElementById("AEtypeDropDown").value;
cameras[ci].setAutoExposureMode({mode: AEmode, type: AEtype});
}
// 0: lift, 1: gamma, 2: gain, 3: offset
function setCCFromUI(which) {
let lumaFloat = parseFloat(document.getElementsByClassName("CClumaLabel")[which].innerHTML);
let redFloat = parseFloat(document.getElementsByClassName("CCredLabel")[which].innerHTML);
let greenFloat = parseFloat(document.getElementsByClassName("CCgreenLabel")[which].innerHTML);
let blueFloat = parseFloat(document.getElementsByClassName("CCblueLabel")[which].innerHTML);
let response = sendRequest(requestMethod,requestURL,requestData);
let ccobject = {"red": redFloat, "green": greenFloat, "blue": blueFloat, "luma": lumaFloat};
if (which == 0) {
cameras[ci].setCCLift(ccobject);
} else if (which == 1) {
cameras[ci].setCCGamma(ccobject);
} else if (which == 2) {
cameras[ci].setCCGain(ccobject);
} else {
cameras[ci].setCCOffset(ccobject);
}
document.getElementById("manualRequestResponseP").innerHTML = JSON.stringify(response);
}
// 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].setCCLift({"red": 0.0, "green": 0.0, "blue": 0.0, "luma": 0.0});
} else if (which == 1) {
cameras[ci].setCCGamma({"red": 0.0, "green": 0.0, "blue": 0.0, "luma": 0.0});
} else if (which == 2) {
cameras[ci].setCCGain({"red": 1.0, "green": 1.0, "blue": 1.0, "luma": 1.0});
} else if (which == 3) {
cameras[ci].setCCOffset({"red": 0.0, "green": 0.0, "blue": 0.0, "luma": 0.0});
} else if (which == 4) {
cameras[ci].setCCContrast({"pivot": 0.5, "adjust": 1.0});
} else if (which == 5) {
cameras[ci].setCCColor({"hue": 0.0, "saturation": 1.0});
cameras[ci].setCCLumaContribuion({"lumaContribution": 1.0});
}
/* 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;
}