diff --git a/web-ui/BMD-Camera-Control.js b/BMD-Camera-Control.js similarity index 97% rename from web-ui/BMD-Camera-Control.js rename to BMD-Camera-Control.js index cf1c0cb..9d0e43a 100644 --- a/web-ui/BMD-Camera-Control.js +++ b/BMD-Camera-Control.js @@ -186,16 +186,16 @@ class BMDCamera { updateUIPresets() { var presetsList = document.getElementById("presetsDropDown"); + presetsList.innerHTML = ""; + this.presets.forEach((presetItem) => { let presetName = presetItem.split('.', 1); - if (!presetsList.contains(document.getElementsByName("presetOption"+presetName)[0])) { - let textNode = document.createTextNode(presetName); - let optionNode = document.createElement("option"); - optionNode.setAttribute("name", "presetOption"+presetName); - optionNode.appendChild(textNode); - document.getElementById("presetsDropDown").appendChild(optionNode); - } + let textNode = document.createTextNode(presetName); + let optionNode = document.createElement("option"); + optionNode.setAttribute("name", "presetOption"+presetName); + optionNode.appendChild(textNode); + document.getElementById("presetsDropDown").appendChild(optionNode); }); } @@ -248,6 +248,10 @@ class BMDCamera { updateUINDStop() { document.getElementById("ndFilterSpan").innerHTML = this.NDStop; + if (this.UnimplementedFunctionality.includes("/video/ndFilter")) { + document.getElementById("ndFilterSpan").innerHTML = 0; + document.getElementById("ndFilterSpan").disabled = true; + } } updateUIshutter() { diff --git a/README.md b/README.md index 2c90002..0c8de28 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,85 @@ # BM Camera Control WebUI - A demonstration of the Blackmagic Camera Control protocols via an extensible web interface +This web app is a demonstration of the [Blackmagic Design](https://blackmagicdesign.com) Camera Control REST API via an extensible web interface. Modeled after the interface of ATEM Software Control, most of the major functions of the camera that can be controlled by the API are available here. + +>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 many features of 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. + +![Screenshot 1](screenshots/WebUI1.png) + +# Getting Started + +## Camera Setup +In order for the camera's API to be active, network connectivity must be enabled in **Blackmagic Camera Setup**, and the camera should be connected to the same network as your computer with an ethernet cable. +
+If your camera does not have an ethernet port, use a USB-C to ethernet adapter. + +>Make sure that your camera has been updated to the latest firmware! (8.6+) + +![BM Camera Setup](screenshots/BMCameraSetup2.png) + +## Launching the App +The app is a self-contained, offline web page. (No installation, dependencies, or Node.js installations to worry about!) Simply open the `index.html` file in your browser of choice, enter the hostname of your camera, and press "Connect". +

+If you don't know the hostname of your camera, you can find and/or change it in **Blackmagic Camera Setup**. +
+(The hostname is the camera's name with spaces replaced with dashes, and `.local` appended to the end) +

+Not all camera and lens combinations are supported by the API (Some cameras have ND filters, some don't. Some electronic zoom lenses work, some don't.) + +# Using the App + +### Data Synchronization +The app polls data from the camera every ten seconds (you'll see "Refreshing..." in the corner). When you change a setting in the browser, it relays that to the camera and verifies the change. If you make a change that reverts after a few seconds, that means the camera rejected it. The page can be manually refreshed with the button in the bottom left corner. + +### Arrows, Buttons, and Text Boxes +Many controls work both with sliders/buttons, but if you want to enter a specific value then click on the number and enter the value manually. +
+(Sometimes this might take a couple of tries). +
+ +### Media Management +To view files on the drives of your camera, follow the link in the bottom-right corder for the **Web Media Manager**. This will take you to *your camera's* internal web server where you can view, download, and upload video files over the network. + +### Manual API Calls +The page allows for the sending of manual API calls to the camera. Use the text boxes to do that, after consulting the documentation. + +### Layout +I have done my best to make the page responsive, but every screen is different. If something looks off, adjust the zoom/scale of the window in your browser and that should fix things. + +# Compatibility +This app (as of June 2024), should be compatible with the following Blackmagic cameras: +| Camera Name | Default Hostname | Notes | +|-|-|-| +| Pocket Cinema Camera 4K | `Pocket-Cinema-Camera-4K.local` | FW 8.6+ Required | +| Pocket Cinema Camera 6K | `Pocket-Cinema-Camera-6K.local` | FW 8.6+ Required | +| Pocket Cinema Camera 6K Pro | `Pocket-Cinema-Camera-6K-Pro.local` | FW 8.6+ Required | +| Cinema Camera 6K | `Cinema-Camera-6K.local` | | +| URSA Broadcast G2 | `URSA-Broadcast-G2.local`$^1$ | | +| Micro Studio Camera 4K G2 | `Micro-Studio-Camera-4K-G2.local`$^1$ | | +| Studio Camera 4K Plus | `Studio-Camera-4K-Plus.local` | | +| Studio Camera 4K Pro | `Studio-Camera-4K-Pro.local` | | +| Studio Camera 4K Plus G2 | `Studio-Camera-4K-Plus-G2.local`$^1$ | | +| Studio Camera 4K Pro G2 | `Studio-Camera-4K-Pro-G2.local`$^1$ | | +| Studio Camera 6K Pro | `Studio-Camera-6K-Pro.local` | | + +$^1:$ Unverified best guess
+If any of this information is incorrect, please let me know in the Issues section of this repository. + +# Issues and To-Dos +## Known Issues +- Page responsiveness +- Editable spans are hard to use + +## Unknown Issues +Please report issues to the repo's issue tracker so I can fix them! +
+If you're having trouble and don't know why, check the browser console. + +## To-Do +- Use WebSockets instead of polling to keep the page in sync +- Make a better UI for color correction +- Add audio settings +- Add codec/format switching settings +- Improve responsiveness +- Improved error handling \ No newline at end of file diff --git a/web-ui/Screenshot 2024-06-12 171720.png b/Screenshot 2024-06-12 171720.png similarity index 100% rename from web-ui/Screenshot 2024-06-12 171720.png rename to Screenshot 2024-06-12 171720.png diff --git a/web-ui/index.html b/index.html similarity index 98% rename from web-ui/index.html rename to index.html index 083c4ce..4da8341 100644 --- a/web-ui/index.html +++ b/index.html @@ -192,7 +192,7 @@ Hostname - + @@ -213,7 +213,7 @@ -

Send manual API requests using the above controls. See documentation for details.

+

Send manual API requests using the above controls. See documentation for details.

diff --git a/web-ui/resources/NotoSansDisplay-VariableFont_wdth,wght.ttf b/resources/NotoSansDisplay-VariableFont_wdth,wght.ttf similarity index 100% rename from web-ui/resources/NotoSansDisplay-VariableFont_wdth,wght.ttf rename to resources/NotoSansDisplay-VariableFont_wdth,wght.ttf diff --git a/web-ui/resources/NotoSansDisplay-VariableFont_wdth,wght.woff b/resources/NotoSansDisplay-VariableFont_wdth,wght.woff similarity index 100% rename from web-ui/resources/NotoSansDisplay-VariableFont_wdth,wght.woff rename to resources/NotoSansDisplay-VariableFont_wdth,wght.woff diff --git a/screenshots/WebUI1.png b/screenshots/WebUI1.png new file mode 100644 index 0000000..ba6f174 Binary files /dev/null and b/screenshots/WebUI1.png differ diff --git a/web-ui/style.css b/style.css similarity index 97% rename from web-ui/style.css rename to style.css index a987ea4..9990cb7 100644 --- a/web-ui/style.css +++ b/style.css @@ -1,5 +1,16 @@ /* ============= WHOLE PAGE STYLES ================== */ +/* Handle vertical screens */ +@media screen and (max-width: 1000px) { + #cameraControlsContainer { + width: 100vw!important; + } + + #cameraControlsContainerExpanded { + display: none; + } +} + /* Load NotoSansDisplay Font from resources */ @font-face { font-family: 'NotoSansDisplay'; diff --git a/web-ui/web-ui.js b/web-ui.js similarity index 77% rename from web-ui/web-ui.js rename to web-ui.js index 1061c57..5da9d11 100644 --- a/web-ui/web-ui.js +++ b/web-ui.js @@ -3,6 +3,7 @@ var cameras = []; var ci = 0; var WBMode = 0; // 0: balance, 1: tint + function bodyOnLoad() { let intervalIDOne = setInterval(timerCallFunction1, 1000); // Tem second timer for refreshing everything let intervalIDTen = setInterval(timerCallFunction10, 10000); // Tem second timer for refreshing everything @@ -18,6 +19,7 @@ function initCamera(hostname, ind) { sendRequest("GET", "http://"+hostname+"/control/api/v1/system","").then((response) => { if (response.status < 300) { cameras[ci] = new BMDCamera(hostname, ind); + document.getElementById("connectionErrorSpan").innerHTML = ""; } else { document.getElementById("connectionErrorSpan").innerHTML = response.statusText; } @@ -64,6 +66,7 @@ function switchCamera(index) { } document.getElementById("cameraNumberLabel").innerHTML = "CAM"+(ci+1); + document.getElementById("cameraName").innerHTML = "CAMERA NAME"; } function setCCMode(mode) { @@ -233,95 +236,38 @@ function resetCC(which) { } 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.0, "adjust": 1.0}); + 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}); } } -function makeFakeCamera() { - cam = new BMDCamera("Studio-Camera-6K-Pro.local",0) - return Object.assign(cam,{ - "name": "Studio Camera 6K Pro", - "hostname": "Studio-Camera-6K-Pro.local", - "APIAddress": "http://Studio-Camera-6K-Pro.local/control/api/v1", - "index": 0, - "transportMode": { - "mode": "InputPreview" - }, - "playbackState": { - "loop": false, - "position": 0, - "singleClip": false, - "speed": 0, - "type": "Play" - }, - "recordState": { - "recording": false - }, - "timecode": { - "clip": 0, - "timecode": 289550880, - "source": "Clip" - }, - "presets": { - "presets": [] - }, - "activePreset": "default", - "apertureStop": 4.400000095367432, - "apertureNormalised": 0.021739130839705467, - "zoomMM": 18, - "zoomNormalised": 0, - "focusNormalised": 0.5, - "ISO": 400, - "gain": 0, - "NDStop": 0, - "NDMode": "Fraction", - "shutter": { - "continuousShutterAutoExposure": false, - "shutterSpeed": 50 - }, - "AutoExposureMode": { - "mode": "Off", - "type": "" - }, - "CClift": { - "blue": 0, - "green": 0, - "luma": 0, - "red": 0 - }, - "CCgamma": { - "blue": 0, - "green": 0, - "luma": 0, - "red": 0 - }, - "CCgain": { - "blue": 1, - "green": 1, - "luma": 1, - "red": 1 - }, - "CCoffset": { - "blue": 0, - "green": 0, - "luma": 0, - "red": 0 - }, - "CCcontrast": { - "adjust": 1, - "pivot": 0.5 - }, - "CCcolor": { - "hue": 0, - "saturation": 1 - }, - "CClumacontribution": { - "lumaContribution": 1 - }, - "WhiteBalance": 5600, - "WhiteBalanceTint": 0 - }) + +/* Cookie Setting functions from StackOverflow :P */ +function createCookie(name, value, days) { + var expires; + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toGMTString(); + } else { + expires=""; + } + document.cookie = name + "=" + value + expires + "; path=/"; +} + +function getCookie(c_name) { + if (document.cookie.length > 0) { + c_start = document.cookie.indexOf(c_name + "="); + if (c_start != -1) { + c_start = c_start + c_name.length + 1; + c_end = document.cookie.indexOf(";", c_start); + if (c_end == -1) { + c_end = document.cookie.length; + } + return unescape(document.cookie.substring(c_start, c_end)); + } + } + return ""; } \ No newline at end of file