/* 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. 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("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/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; } 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}); } doAutoFocus() { this.PUTdata("/lens/focus/doAutoFocus"); } } /* 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 */