BM-Camera-Control-WebUI/BMDevice.js

288 lines
9.6 KiB
JavaScript
Raw Normal View History

2024-07-02 17:53:25 -04:00
/* 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, use with HyperDecks.
2024-07-02 17:53:25 -04:00
class BMDevice {
// Pretty name and network hostname (strings)
name;
hostname;
APIAddress;
2024-07-09 12:46:16 -04:00
// Are we using HTTPS?
useHTTPS;
2024-07-02 17:53:25 -04:00
// 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 ================
2024-07-09 12:46:16 -04:00
constructor(hostname, secure=false) {
// Set Security
this.useHTTPS = secure;
2024-07-02 17:53:25 -04:00
// Set name properties
this.hostname = hostname;
2024-07-09 12:46:16 -04:00
this.APIAddress = (this.useHTTPS ? "https://" : "http://")+hostname+"/control/api/v1";
2024-07-02 17:53:25 -04:00
this.name = this.hostname.replace(".local","").replaceAll("-"," ");
// Initialize WebSocket
2024-07-09 12:48:27 -04:00
this.ws = new WebSocket((this.useHTTPS ? "wss://" : "ws://")+hostname+"/control/api/v1/event/websocket");
2024-07-02 17:53:25 -04:00
// 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;
2024-07-02 18:12:08 -04:00
let playbackData = this.GETdata("/transports/0/playback");
2024-07-02 17:53:25 -04:00
let runningSum = 0;
2024-07-02 18:12:08 -04:00
let currentClipFound = false;
2024-07-02 17:53:25 -04:00
let currentClipIndex = 0;
let clipStartingTimecodes = [];
let i = 0;
clips.forEach((clip) => {
2024-07-02 18:12:08 -04:00
if ((runningSum+clip.frameCount > playbackData.position) && !currentClipFound) {
2024-07-02 17:53:25 -04:00
currentClipIndex = i;
2024-07-02 18:12:08 -04:00
currentClipFound = true;
2024-07-02 17:53:25 -04:00
}
clipStartingTimecodes[i] = runningSum;
runningSum += clip.frameCount;
i++;
});
2024-07-02 18:12:08 -04:00
let newClipIndex = Math.min(Math.max(0,(direction ? currentClipIndex+1 : currentClipIndex-1)), clips.length-1);
2024-07-02 17:53:25 -04:00
playbackData.position = clipStartingTimecodes[newClipIndex];
2024-07-02 18:12:08 -04:00
this.PUTdata("/transports/0/playback", playbackData);
2024-07-02 17:53:25 -04:00
}
// 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
2024-07-09 12:55:57 -04:00
// Just passing the hostname and security to the superclass's constructor
constructor(hostname, secure=false) {
super(hostname, secure);
2024-07-02 17:53:25 -04:00
}
// 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});
}
doAutoFocus() {
this.PUTdata("/lens/focus/doAutoFocus");
}
2024-08-28 10:30:41 -04:00
doAutoWhitebalance() {
this.PUTdata("/video/whiteBalance/doAuto");
}
2024-07-02 17:53:25 -04:00
}
/* 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 */