diff --git a/web-ui/BMD-Camera-Control.js b/web-ui/BMD-Camera-Control.js new file mode 100644 index 0000000..2493558 --- /dev/null +++ b/web-ui/BMD-Camera-Control.js @@ -0,0 +1,409 @@ +class BMDCamera { + // Pretty name and network hostname (strings) + name; + hostname; + APIAddress; + + // Camera index, used for muticam support + index; + + // == TODO: Having trouble with the codec and video formats on the SC 6K Pro == + // Codec and Video Formats (JSON objects) + codecFormat; + videoFormat; + + // Supported Codecs/Videos (arrays) + supportedCodecFormats; + supportedVideoFormats; + // ============================================================================ + + // Current Transport Mode (string) + transportMode; + + // Whether the transport is playing or not (boolean) + isPlaying; + + // Playback state (JSON object) + playbackState; + + // Record state (JSON object) + recordState; + + // Timecode (JSON Object) + timecode; + // (pack the source into here also) + + // Presets (JSON object) + presets; + activePreset; + + // Iris (floats) + apertureStop; + apertureNormalised; + + // Zoom (floats) + zoomMM; + zoomNormalised; + + // Focus (float) + focusNormalised; + + // ISO (int) + ISO; + + // Gain (int) + gain; + + // White Balance (ints) + WhiteBalance; + WhiteBalanceTint; + + // ND Filter (int, string) + NDStop; + NDMode; + + // Shutter (JSON object) + shutter; + // has to be an object because it either returns with shutterSpeed or shutterAngle + + // AE Mode (JSON Object) + AutoExposureMode; + + // Basic Color Correction (JSON objects w/ RGBL) + CClift; + CCgamma; + CCgain; + CCoffset; + + // Other Color Correction (JSON objects w/ 2 numbers) + CCcontrast; + CCcolor; + CClumacontribution; + + // ============= CONSTRUCTOR ================ + constructor(hostname, index) { + this.hostname = hostname; + this.index = index; + this.APIAddress = "http://"+hostname+"/control/api/v1"; + this.name = this.hostname.replace(".local","").replaceAll("-"," "); + + this.refresh(); + } + + // Important refreshing function + refresh() { + this.getAllInfo(); + sleep(500).then(() => + this.updateUIAll() + ); + } + + // Wrapper for API call, returns the JSON object from the camera + async pullData(endpoint) { + return await sendRequest("GET",this.APIAddress+endpoint,""); + } + + // Wrapper for API call, returns whatever the camera sent back in response + async pushData(endpoint, data) { + return await sendRequest("PUT",this.APIAddress+endpoint,data); + } + + // ======= UI Updaters ========== + updateUIAll() { + this.updateUIname(); + this.updateUIhostname(); + this.updateUICodecFormat(); + this.updateUIVideoFormat(); + this.updateUISupportedCodecFormats(); + this.updateUISupportedVideoFormats(); + this.updateUITransportMode(); + this.updateUIisPlaying(); + this.updateUIPlaybackState(); + this.updateUIRecordState(); + this.updateUITimecode(); + this.updateUIPresets(); + this.updateUIActivePreset(); + this.updateUIAperture(); + this.updateUIZoom(); + this.updateUIFocus(); + this.updateUIISO(); + this.updateUIgain(); + this.updateUIWhiteBalance(); + this.updateUINDStop(); + this.updateUIshutter(); + this.updateUIAutoExposureMode(); + this.updateUIColorCorrection(); + } + + updateUIname() { + document.getElementsByClassName("cameraName")[this.index].innerHTML = this.name; + } + + updateUIhostname() { + //TBD + } + + updateUICodecFormat() { + //TBD + } + + updateUIVideoFormat() { + //TBD + } + + updateUISupportedCodecFormats() { + //TBD + } + + updateUISupportedVideoFormats() { + //TBD + } + + updateUITransportMode() { + //TBD + } + + updateUIisPlaying() { + //TBD + } + + updateUIPlaybackState() { + //TBD + } + + updateUIRecordState() { + if (this.recordState.recording) { + document.getElementsByClassName("cameraControlsContainer")[this.index].classList.add("liveCam"); + } else { + document.getElementsByClassName("cameraControlsContainer")[this.index].classList.remove("liveCam"); + } + } + + updateUITimecode() { + var tcString = parseInt(this.timecode.timecode.toString(16),10).toString().match(/.{1,2}/g).join(':'); + + document.getElementsByClassName("timecodeLabel")[this.index].innerHTML = tcString; + } + + updateUIPresets() { + //TBD + } + + updateUIActivePreset() { + //TBD + } + + updateUIAperture() { + document.getElementsByClassName("irisRange")[this.index].value = this.apertureNormalised; + document.getElementsByClassName("apertureStopsLabel")[this.index].innerHTML = this.apertureStop.toFixed(1); + } + + updateUIZoom() { + document.getElementsByClassName("zoomRange")[this.index].value = this.zoomNormalised; + document.getElementsByClassName("zoomMMLabel")[this.index].innerHTML = this.zoomMM; + } + + updateUIFocus() { + document.getElementsByClassName("focusRange")[this.index].value = this.focusNormalised; + } + + updateUIISO() { + // TBD + } + + updateUIgain() { + var gainString = ""; + + if (this.gain >= 0) { + gainString = "+"+this.gain+"db" + } else { + gainString = this.gain+"db" + } + + document.getElementsByClassName("gainSpan")[this.index].innerHTML = gainString; + } + + updateUIWhiteBalance() { + document.getElementsByClassName("whiteBalanceSpan")[this.index].innerHTML = this.WhiteBalance+"K"; + } + + updateUINDStop() { + document.getElementsByClassName("ndFilterSpan")[this.index].innerHTML = this.NDStop; + } + + updateUIshutter() { + var shutterString = "" + + if ('shutterSpeed' in this.shutter) { + shutterString = "1/"+this.shutter.shutterSpeed + } else { + var shangleString = this.shutter.shutterAngle.toString(); + shutterString = shangleString.slice(0,3)+(shangleString.slice(3,4) == '0' ? '' : "."+shangleString.slice(3,4))+"°" + } + + document.getElementsByClassName("shutterSpan")[this.index].innerHTML = shutterString; + } + + updateUIAutoExposureMode() { + //TBD + } + + updateUIColorCorrection() { + //TBD + } + + // =============== GETTERS ================== + + // name, hostname, APIaddress, index handled by constructor + + getCodecFormat() { + this.pullData("/system/codecFormat").then((value) => {this.codecFormat = value}); + } + + getVideoFormat() { + this.pullData("/system/videoFormat").then((value) => {this.videoFormat = value}); + } + + getSupportedCodecFormats() { + this.pullData("/system/supportedCodecFormats").then((value) => {this.supportedCodecFormats = value}); + } + + getSupportedVideoFormats() { + this.pullData("/system/supportedVideoFormats").then((value) => {this.supportedVideoFormats = value}); + } + + getTransportMode() { + this.pullData("/transports/0").then((value) => {this.transportMode = value}); + } + + getIsPlaying() { + this.pullData("/transports/0/play").then((value) => {this.isPlaying = value}); + } + + getPlaybackState() { + this.pullData("/transports/0/playback").then((value) => {this.playbackState = value}); + } + + getRecordState() { + this.pullData("/transports/0/record").then((value) => {this.recordState = value}); + } + + getTimecode() { + this.pullData("/transports/0/timecode").then((value) => {this.timecode = value}); + this.pullData("/transports/0/timecode/source").then((value) => {this.timecode.source = value.source}); + } + + getPresets() { + this.pullData("/presets").then((value) => {this.presets = value.presets}); + } + + getActivePreset() { + this.pullData("/presets/active").then((value) => {this.activePreset = value}); + } + + getAperture() { + this.pullData("/lens/iris").then((value) => {this.apertureStop = value.apertureStop; this.apertureNormalised = value.normalised}); + } + + getZoom() { + this.pullData("/lens/zoom").then((value) => {this.zoomMM = value.focalLength; this.zoomNormalised = value.normalised}); + } + + getFocus() { + this.pullData("/lens/focus").then((value) => {this.focusNormalised = value.normalised}); + } + + getISO() { + this.pullData("/video/iso").then((value) => {this.ISO = value.iso}); + } + + getGain() { + this.pullData("/video/gain").then((value) => {this.gain = value.gain}); + } + + getWhiteBalance() { + this.pullData("/video/whiteBalance").then((value) => {this.WhiteBalance = value.whiteBalance}); + this.pullData("/video/whiteBalanceTint").then((value) => {this.WhiteBalanceTint = value.whiteBalanceTint}); + } + + getND() { + this.pullData("/video/ndFilter").then((value) => {this.NDStop = value.stop}); + this.pullData("/video/ndFilter/displayMode").then((value) => {this.NDMode = value.displayMode}); + } + + getShutter() { + this.pullData("/video/shutter").then((value) => {this.shutter = value}); + } + + getAutoExposureMode() { + this.pullData("/video/autoExposure").then((value) => {this.AutoExposureMode = value}); + } + + getColorCorrection() { + this.pullData("/colorCorrection/lift").then((value) => {this.CClift = value}); + this.pullData("/colorCorrection/gamma").then((value) => {this.CCgamma = value}); + this.pullData("/colorCorrection/gain").then((value) => {this.CCgain = value}); + this.pullData("/colorCorrection/offset").then((value) => {this.CCoffset = value}); + this.pullData("/colorCorrection/contrast").then((value) => {this.CCcontrast = value}); + this.pullData("/colorCorrection/color").then((value) => {this.CCcolor = value}); + this.pullData("/colorCorrection/lumaContribution").then((value) => {this.CClumacontribution = value}); + } + + getAllInfo() { + this.getCodecFormat(); + this.getVideoFormat(); + this.getSupportedCodecFormats(); + this.getSupportedVideoFormats(); + this.getTransportMode(); + this.getIsPlaying(); + this.getPlaybackState(); + this.getRecordState(); + this.getTimecode(); + this.getPresets(); + this.getActivePreset(); + this.getAperture(); + this.getZoom(); + this.getFocus(); + this.getISO(); + this.getGain(); + this.getWhiteBalance(); + this.getND(); + this.getShutter(); + this.getAutoExposureMode(); + this.getColorCorrection(); + } + + // =============== Other Commands ======================= + doAutoFocus() { + this.pushData("/lens/focus/doAutoFocus") + } + + /* Timer Stuff */ + everySecond() { + this.refresh(); + } +} + +/* Helper Functions */ +async function sendRequest(method, url, data) { + const xhttp = new XMLHttpRequest(); + var responseObject; + + // TODO: Add error code handling + xhttp.onload = function() { + if (this.responseText) { + responseObject = JSON.parse(this.responseText); + } else { + responseObject = {"status": this.statusText}; + } + } + + xhttp.open(method, url, false); + xhttp.send(JSON.stringify(data)); + + return responseObject; +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} \ No newline at end of file diff --git a/web-ui/index.html b/web-ui/index.html new file mode 100644 index 0000000..b443a8b --- /dev/null +++ b/web-ui/index.html @@ -0,0 +1,102 @@ + + + + + Blackmagic Camera Control WebUI + + + + + + + + + + + + + + + +
+

Blackmagic Camera Control WebUI

+
+ + +
+ +
+ + +
+
+
+

CAM1

+
+ +
+ +
+ +
+
+ FILTER + + 0 +
+
+ GAIN + + +0db +
+
+ SHUTTER + + 1/50 +
+
+ BALANCE + + 5600K +
+
+ +
+
+ FOCUS + + +
+
+ IRIS + + X.X +
+
+ ZOOM + + XXmm +
+
+ +
+ +
+
+

TIMECODE

+
+
+ Hostname: + + +
+
+ +
+ + +
+ +
+ + \ No newline at end of file diff --git a/web-ui/resources/NotoSansDisplay-VariableFont_wdth,wght.ttf b/web-ui/resources/NotoSansDisplay-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..04f51c4 Binary files /dev/null and b/web-ui/resources/NotoSansDisplay-VariableFont_wdth,wght.ttf differ diff --git a/web-ui/resources/NotoSansDisplay-VariableFont_wdth,wght.woff b/web-ui/resources/NotoSansDisplay-VariableFont_wdth,wght.woff new file mode 100644 index 0000000..13c9151 Binary files /dev/null and b/web-ui/resources/NotoSansDisplay-VariableFont_wdth,wght.woff differ diff --git a/web-ui/style.css b/web-ui/style.css new file mode 100644 index 0000000..0e34d91 --- /dev/null +++ b/web-ui/style.css @@ -0,0 +1,218 @@ +/* Load NotoSansDisplay Font from resources */ +@font-face { + font-family: 'NotoSansDisplay'; + src: url('resources/NotoSansDisplay-VariableFont_wdth\,wght.woff') format('woff'), /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ + url('resources/NotoSansDisplay-VariableFont_wdth\,wght.ttf') format('truetype'); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */ +} + +body { + font-family: 'NotoSansDisplay', sans-serif; + margin: 0px; + overflow: hidden; + background: #181818; + color: white; +} + +/* Page Body Flexboxes */ +.flexContainerH { + display: flex; +} + +.flexContainerV { + display: inline-flex; +} + +/* Horizontal Container Styles */ +#headerContainer { + background: #181818; + background: linear-gradient(0deg, #181818 0%, #303030 100%); + border-bottom: 1px solid black; + width: 100%; + height: 7.77vh; + flex-wrap: wrap; + align-content: center; +} + +#headerContainer h1 { + font-weight: 100; + color: white; + margin-left: 1.3vw; +} + +#cameraSelectContainer { + background: #222222; + border: 1px solid black; + width: 100%; + height: 3.53vh; +} + +#allCamerasContainer { + width: 100%; + height: 83.1vh; + + overflow-x: scroll; + overflow-y: hidden; + + scrollbar-color: #202020 #151515; +} + +#footerContainer { + background: #181818; + background: linear-gradient(0deg, #181818 0%, #303030 100%); + border: 1px solid black; + width: 100%; + height: 5vh; + position: fixed; + bottom: 0; +} + +/* Camera Controls Container */ +.cameraControlsContainer { + width: 15vw; + height: 100%; + background: #282828; + flex-direction: column; + border: 1px solid black; + flex-shrink: 0; +} + +.cameraControlsContainer.selectedCam { + background: #323232; +} + +.liveCam .cameraControlHeadContainer { + background: rgb(184,3,16); + background: linear-gradient(90deg, rgba(184,3,16,1) 0%, rgba(255,0,19,1) 15%, rgba(255,0,19,1) 85%, rgba(184,3,16,1) 100%); +} + +.cameraControlHeadContainer { + width: 100%; + height: 5vh; + border-bottom: 2px solid black; + align-items: center; +} + +.cameraName { + font-weight: 100; + color: white; + margin-inline-start: 0.6em; + margin-inline-end: 0.6em; +} + +/* Color Correction Section */ +.cameraControlColorCorrectionContainer { + width: 100%; + height: 33vh; +} + +/* Exposure Section */ +.cameraControlExposureContainer { + width: 100%; + height: 4.4vh; + background-color: #171717; + border-top: 1px solid #2d2d2d; + border-bottom: 1px solid #2d2d2d; + display: inline-flex; + justify-content: space-evenly; + overflow: hidden; +} + +.selectedCam .cameraControlExposureContainer { + border-top: 1px solid #3a3a3a; + border-bottom: 1px solid #3a3a3a; +} + +.ccExposureSettingContainer { + display: flex; + color: white; + font-size: 0.86em; + flex-direction: column; + align-content: center; + justify-content: center; + align-items: center; + padding-bottom: 0.5vh; + padding-top: 0.25vh; +} + +.exposureControlLabel { + color: #6e6e6e; + font-size: 0.666em; + display: block; +} + +/* Lens Stuff */ +.cameraControlLensContainer { + width: 100%; + height: 41.9vh; + border-bottom: 1px solid black; + justify-content: space-evenly; +} + +.lensSliderContainer { + display: flex; + flex-direction: column; + align-items: center; +} + +.lensSliderContainer span { + margin-top: 1em; + margin-bottom: 1em; +} + +.lensSliderContainer button { + margin-top: 1em; + margin-bottom: 1em; +} + +input[type=range][orient=vertical] { + -webkit-appearance: slider-vertical; + width: 2vw; + height: 80%; +} + +/* Right side (expanded) */ +.cameraControlsContainerExpanded { + width: 84.75vw; + height: 100%; + background: #282828; + flex-direction: column; + border: 1px solid black; + flex-shrink: 0; +} + +.cameraControlExpandedHeadContainer { + width: 100%; + height: 5vh; + border-bottom: 2px solid black; + align-items: center; + justify-content: center; +} + +.timecodeLabel { + font-weight: 100; + color: white; + margin-inline-start: 0.6em; + margin-inline-end: 0.6em; +} + +/* Connection Settings */ +.connectionContainer { + margin: 1.5vw; +} + +input[type=text] { + border-radius: 0.5vh; + background: rgb(30, 30, 30); + color: white; + height: 2em; + width: 10vw; + border: 1px solid rgb(20, 20, 20); + margin-left: 1vw; + text-align: center; + font-family: 'NotoSansDisplay', sans-serif; + outline: none; +} + +input[type=text]:focus { + border: 1px solid rgb(150, 58, 0); +} \ No newline at end of file diff --git a/web-ui/web-ui.js b/web-ui/web-ui.js new file mode 100644 index 0000000..41c1c23 --- /dev/null +++ b/web-ui/web-ui.js @@ -0,0 +1,110 @@ +/* Global variables */ +var cameras = []; +var ci = 0; + +function bodyOnLoad() { + //let intervalID = setInterval(timerCallFunction, 1000); + + let newCamHostname = document.getElementsByClassName("hostnameInput")[ci].value; + + if (newCamHostname) { + cameras[ci] = new BMDCamera(newCamHostname,ci); + } +} + +// function timerCallFunction() { +// cameras.forEach((camera) => camera.everySecond()); +// } + +function textInputTrigger(element) { + if (event.key === 'Enter') { + cameras[ci] = new BMDCamera(element.value, ci); + } +} + +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" + }, + "isPlaying": false, + "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 + }) +} \ No newline at end of file