# 4. Conducting an Orchestra
Welcome back! In this article, we'll talk about ways to organize the data and API calling functions for _multiple_ devices! We'll talk about
object-oriented programming, how to write classes, and even control other Blackmagic devices in addition to cameras.
## Object-Oriented Programming
Object-Oriented Programming (OOP) is a paradigm used in Computer Science for programs that associate data and functions with **classes** and **objects**. Classes are comparable to a
template or a blueprint. They define what properties and behaviors an object can have, and when the program runs, we create **instances** of that class which store their data in the way
defined by the class.
A good example of OOP is what we'll be implementing here. We will make a class that represents a camera, and it will hold all of the relevant data for that camera such as its
hostname and settings we fetch. The class we make will have methods that send commands to the camera to push or fetch data to/from the camera.
Implementing this functionality with OOP will allow us to keep the code more organized and reusable than if we had to write independent functions for everything. From here
on out, I'll assume that you have a basic idea of classes, objects, and instances in your programming language of choice. If you're working in JavaScript along
with me, you should be able to get the basic understanding from the examples below. For my sanity, I'll only be working in JS from here on out, but know that
everything I show here will have an equivalent in Python, if that's what you're using. Without further adieu, let's go!
## Classes and Methods
In a new JavaScript file (I'm in `examples/Camera.js`), we'll declare a new class called `Camera` like so:
```JS
class Camera {
}
```
For now, let's store some basic information about the camera in class fields. These fields can have different values
for each `Camera` object we instantiate.
```JS
class Camera {
// Keep track of the camera's hostname and API address
hostname;
APIAddress;
}
```
When we make a new `Camera` object, we want to make sure these values are set correctly. Since objects are
instantiated with the class's constructor, we can add the constructor like so:
```JS
constructor(hostname) {
this.hostname = hostname;
this.APIAddress = "http://"+hostname+"/control/api/v1";
}
```
The `this` keyword is important here. `this.hostname` refers to the `hostname` field of the instance created
by the constructor. `this` will show up a _lot_ when writing class methods in JavaScript.
Next, we want to give this class the ability to talk to the camera. I'll take our `sendGETRequest` and `sendPUTRequest`
functions from earlier and transform them into methods for the camera class that make use of the API address we have stored.
Adding those methods, our `Camera` class now looks like this:
```JS
class Camera {
// Keep track of the camera's hostname and API address
hostname;
APIAddress;
// Constructor takes hostname as an argument and sets the
// hostname and APIAddress fields accordingly
constructor(hostname) {
this.hostname = hostname;
this.APIAddress = "http://"+hostname+"/control/api/v1";
}
// Returns a JSON Object of data we got from the camera
GETdata(endpoint) {
// Instantiate the XMLHttpRequest object
let xhr = new XMLHttpRequest();
// Create an object to store and return the response
var responseObject;
// Define the onload function
xhr.onload = function() {
if (this.status < 300) { // If the operation is successful
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("GET", this.APIAddress+endpoint, false);
// Send the request
xhr.send();
// Return the data
return responseObject;
}
// Send JSON Object data to the camera
PUTdata(endpoint, data) {
// Instantiate the XMLHttpRequest object
let xhr = new XMLHttpRequest();
// Create an object to store and return the response
var 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("PUT", this.APIAddress+endpoint, false);
// Send the request with data
xhr.send(JSON.stringify(data));
// Return response data
return responseObject;
}
}
```
Notice that these methods are not so different from the functions we wrote before. The main difference between them is
that instead of using the API address we declared as a constant, it uses the stored API address of the camera that calls it.
I've also changed the `PUTdata` method to send `JSON.stringify(data)` rather than just `data` so that we don't have to stringify
data when we pass it as an argument.
Loading up this class file, we can make a new `Camera` object named `testCamera` and get some data from it like so:
```JS
var testCamera = new Camera("Studio-Camera-6K-Pro.local"); // Remember to change this to YOUR camera's hostname!
console.log(testCamera.GETdata("/video/iso"));
```
```
Output:
{iso: 3200, status: 200}
```
Success! Now let's try changing a setting.
```JS
var testCamera = new Camera("Studio-Camera-6K-Pro.local"); // Remember to change this to YOUR camera's hostname!
console.log(testCamera.GETdata("/video/whiteBalance"));
testCamera.PUTdata("/video/whiteBalance",{whiteBalance: 3200});
console.log(testCamera.GETdata("/video/whiteBalance"));
```
```
Output:
{whiteBalance: 5600, status: 200}
{whiteBalance: 3200, status: 200}
```
Awesome! We've made it super simple to change settings using methods and class fields. It's now trivial to store
information about the camera's settings within the object. To do that, we can use dot notation to reference instance
fields whether or not they already exist. For example:
```JS
testCamera.ISO = testCamera.GETdata("/video/iso").iso
```
This line of code sets the `ISO` field of the `testCamera` instance of the `Camera` class to whatever `iso` value we receive
from the camera.
We can write complex methods using this data very easily, like setting white balance based on a preset:
```JS
// 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) {
var newWhiteBalance;
var 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});
}
```
How about getting almost all of the camera data in a single line of code?
```JS
testCamera.GETdata("/event/list").forEach((str) => testCamera[str] = testCamera.GETdata(str));
```
or, as a method:
```JS
// Uses the endpoints from calling "/event/list" to populate the object with data
// Not all data is included, such as anything from the "/video" endpoints, but much of it is.
GETdataFromEventList() {
// The "/event/list" endpoint will return an array of endpoints we can query for their status
var eventStrings = this.GETdata("/event/list");
// For each of the strings, set the corresponding field in this object to the camera's current status for that field
eventStrings.forEach((str) => {
// Get data from the camera
var responseData = this.GETdata(str);
// Remove the "status" key from the response
delete responseData["status"];
// Set corresponding field
this[str] = responseData;
});
}
```
This specific method is error-prone and doesn't get _all_ of the data from the camera, but I include it here as an example of
using and modifying data from the camera in a programmatic way.
Remember that `GETdata` returns the JSON data from the camera _with the addition of_ the HTTP status code of the request
in a `status` value. Unless you want to store the status of the request with the data, I'd advise removing it from `GETData`'s returned JSON
object before storing it, as I did in the method above. This makes programming methods that modify and/or send the data back to the camera much easier.
You could also modify `GETdata` to not even include the status in the responseObject, which would require different error handling. Since we aren't doing
any error handling in this tutorial (handling HTTP request errors in JavaScript is beyond its scope), you can choose whether to keep or remove it.
## Multiple Cameras
Because the data and methods for each camera instance are independent, we can make multiple `Camera` instances and control them all
independently. That's why I've named this article "Conducting an Orchestra". Let's see an example making two camera objects and changing
settings on both of them:
```JS
var camera1 = new Camera("Studio-Camera-6K-Pro.local"); // Remember to change these!
var camera2 = new Camera("Blackmagic-Cinema-Camera-6K.local");
camera1.setWhiteBalancePreset(0);
camera2.setWhiteBalancePreset(0);
```
Notice that the cameras need not be the same model for this to work. This opens up some awesome capabilities for synchronizing color correction, white balance,
or even multi-cam recording!
If we add this method to the Camera class:
```JS
// This function will make the camera record
// If the optional parameter is set to false, it will stop recording
record(state = true) {
this.PUTdata("/transports/0/record",{recording: state});
}
```
Then, we can run a script like this to make all of our cameras record at the same time!
```JS
// Make an array to hold all our camera objects
var cameras = [new Camera("Studio-Camera-6K-Pro.local"), new Camera("Blackmagic-Cinema-Camera-6K.local")] // Remember to change these!
// Tell all of them to record
cameras.forEach((cam) => cam.record());
```
## HyperDecks Can Do It Too
Cameras aren't the only Blackmagic device that support the REST API. In fact, devices that use the REST API
use many of the same endpoints. Basic functions like record, play, stop, all work the same between product types.
Some endpoints might return differently between products or return a `501: Not implemented` error, so make sure to test it out.
> Remember that networking functionality must be enabled in the setup utility for all of the devices you're using!
We can refactor our `Camera` class into a `BMDevice` class with minimal modifications:
```JS
// examples/BMDevice.js
class BMDevice {
// Keep track of the device's hostname and API address
hostname;
APIAddress;
// Constructor takes hostname as an argument and sets the
// hostname and APIAddress fields accordingly
constructor(hostname) {
this.hostname = hostname;
this.APIAddress = "http://"+hostname+"/control/api/v1";
}
// Returns a JSON Object of data we got from the device
GETdata(endpoint) {
// Instantiate the XMLHttpRequest object
let xhr = new XMLHttpRequest();
// Create an object to store and return the response
var responseObject;
// Define the onload function
xhr.onload = function() {
if (this.status < 300) { // If the operation is successful
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("GET", this.APIAddress+endpoint, false);
// Send the request
xhr.send();
// Return the data
return responseObject;
}
// Send JSON Object data to the device
PUTdata(endpoint, data) {
// Instantiate the XMLHttpRequest object
let xhr = new XMLHttpRequest();
// Create an object to store and return the response
var 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("PUT", this.APIAddress+endpoint, false);
// Send the request with data
xhr.send(JSON.stringify(data));
// Return response data
return responseObject;
}
// Uses the endpoints from calling "/event/list" to populate the object with data
// Not all data is included, such as anything from the "/video" endpoints, but much of it is.
GETdataFromEventList() {
// The "/event/list" endpoint will return an array of endpoints we can query for their status
var eventStrings = this.GETdata("/event/list");
// For each of the strings, set the corresponding field in this object to the device's current status for that field
eventStrings.forEach((str) => {
// Get data from the device
var responseData = this.GETdata(str);
// Remove the "status" key from the response
delete responseData["status"];
// Set corresponding field
this[str] = responseData;
});
}
// This function will make the device record
// If the optional parameter is set to false, it will stop recording
record(state = true) {
this.PUTdata("/transports/0/record",{recording: state});
}
}
```
This is a good time to talk about class inheritance. Inheritance is a fundamental part of OOP.
A class can inherit fields and methods from another class (called the "superclass"), and use them as just like its own fields and methods.
The subclass can also have its own unique functionality, specific to it. For example, a class representing a car might have
sedan, SUV, or station wagon subclasses that inherit the functionality from the car superclass while adding functions specific to their own models.
We'll do the same here with BMDevice and a _new_ `BMCamera` subclass. Since our white balance setting function only makes sense for cameras, we'll put
that into our `BMCamera` class as well:
```JS
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) {
var newWhiteBalance;
var 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});
}
}
```
Let's put it _all_ together and write a script that records in BRAW on the Studio Camera and H.264 on the HyperDeck
at the same time:
```JS
// Instantiate Device Objects
var camera = new BMCamera("Studio-Camera-6K-Pro.local");
var hyperDeck = new BMDevice("HyperDeck-Extreme-8K-HDR.local");
// Set formats
var newCameraFormat = {"codec": "BRaw:Q3","frameRate": "24","offSpeedEnabled": false,"recordResolution": {"height": 2160,"width": 3840},"sensorResolution": {"height": 2160,"width": 3840}}
var newHyperDeckFormat = {"codec": "H264:Medium","container": "MP4"};
camera.PUTdata("/system/format", newCameraFormat);
hyperDeck.PUTdata("/system/codecFormat",newHyperDeckFormat);
// Tell them to record
camera.record();
hyperDeck.record();
```
This is _super_ useful, and having this level of control over the equipment that can be tied in to
all the features of flexible programming languages like JavaScript and Python means that the
possibilities are literally endless.
In the next article, we'll talk about how you can get media on and off of the devices.
Of course, we'll do it completely over the network. See you there!