-
Notifications
You must be signed in to change notification settings - Fork 0
/
4-Conducting-an-Orchestra.html
568 lines (452 loc) · 26.1 KB
/
4-Conducting-an-Orchestra.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
<!-- Blackmagic REST API Tutorial -->
<!-- Episode 4: Conducting an Orchestra -->
<!-- (c) 2024 Dylan Speiser -->
<!-- Licensed under GNU GPL v3 -->
<!DOCTYPE html>
<html>
<head>
<!-- Page Title and Links -->
<title>4: Conducting an Orchestra - Blackmagic REST API Tutorials</title>
<!-- Link Stylesheets -->
<link rel="stylesheet" href="resources/prism.css">
<link rel="stylesheet" href="resources/stylesheet.css">
<!-- Page metadata-->
<meta charset="UTF-8">
<meta name="description" content="Tutorial series for Blackmagic Camera Control REST API">
<meta name="author" content="Dylan Speiser">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Favicons -->
<link rel="apple-touch-icon" sizes="180x180" href="resources/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="resources/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="resources/favicon/favicon-16x16.png">
</head>
<body class="noto-sans-display">
<!-- Load Scripts -->
<script type="module" src="https://md-block.verou.me/md-block.js"></script>
<script src="resources/prism.js"></script>
<!-- Header Block, same for every page -->
<header>
<h1><a href="index.html">Blackmagic REST API Tutorials</a></h1>
<a href="https://github.com/DylanSpeiser/BM-API-Tutorial">GitHub</a>
</header>
<!-- Next/Previous Page Items -->
<div id="PrevNextBox">
<a href="3-PUT-Requests.html">< Previous Page</a>
<a href="5-Media-Management.html">Next Page ></a>
</div>
<!-- Parse page content from markdown -->
<md-block>
# 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!
</md-block>
<div><a href="5-Media-Management.html" style="text-align: center;">Next Article</a></div>
<!-- Footer -->
<footer>
<div style="text-align: center;">
<br>
© 2024 Dylan Speiser
<br>
GNU GPL v3.0
<br>
All product images and trademarks are Copyright Blackmagic Design.
</div>
</footer>
</body>
</html>