// // SpectatorCamera.qml // qml/hifi // // Spectator Camera v2. // // Created by Zach Fox on 2018-12-12 // Copyright 2018 High Fidelity, Inc. // Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // import Hifi 1.0 as Hifi import QtQuick 2.7 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 import stylesUit 1.0 as HifiStylesUit import controlsUit 1.0 as HifiControlsUit import controls 1.0 as HifiControls import "qrc:////qml//hifi" as Hifi Rectangle { HifiStylesUit.HifiConstants { id: hifi; } id: root; property bool uiReady: false; property bool processingStillSnapshot: false; property bool processing360Snapshot: false; // Style color: "#404040"; // The letterbox used for popup messages Hifi.LetterboxMessage { id: letterboxMessage; z: 998; // Force the popup on top of everything else } function letterbox(headerGlyph, headerText, message) { letterboxMessage.headerGlyph = headerGlyph; letterboxMessage.headerText = headerText; letterboxMessage.text = message; letterboxMessage.visible = true; letterboxMessage.popupRadius = 0; } // // TITLE BAR START // Rectangle { id: titleBarContainer; // Size width: root.width; height: 60; // Anchors anchors.left: parent.left; anchors.top: parent.top; color: "#121212"; // "Spectator" text HifiStylesUit.RalewaySemiBold { id: titleBarText; text: "Spectator Camera 2.7"; // Anchors anchors.left: parent.left; anchors.leftMargin: 30; width: paintedWidth; height: parent.height; size: 22; // Style color: hifi.colors.white; // Alignment horizontalAlignment: Text.AlignHLeft; verticalAlignment: Text.AlignVCenter; } Switch { id: masterSwitch; focusPolicy: Qt.ClickFocus; width: 65; height: 30; anchors.verticalCenter: parent.verticalCenter; anchors.right: parent.right; anchors.rightMargin: 30; hoverEnabled: true; onHoveredChanged: { if (hovered) { switchHandle.color = hifi.colors.blueHighlight; } else { switchHandle.color = hifi.colors.lightGray; } } onClicked: { if (!checked) { flashCheckBox.checked = false; } sendToScript({method: (checked ? 'spectatorCameraOn' : 'spectatorCameraOff')}); sendToScript({method: 'updateCameravFoV', vFoV: fieldOfViewSlider.value}); } background: Rectangle { color: parent.checked ? "#1FC6A6" : hifi.colors.white; implicitWidth: masterSwitch.width; implicitHeight: masterSwitch.height; radius: height/2; } indicator: Rectangle { id: switchHandle; implicitWidth: masterSwitch.height - 4; implicitHeight: implicitWidth; radius: implicitWidth/2; border.color: "#E3E3E3"; color: "#404040"; x: Math.max(4, Math.min(parent.width - width - 4, parent.visualPosition * parent.width - (width / 2) - 4)) y: parent.height / 2 - height / 2; Behavior on x { enabled: !masterSwitch.down SmoothedAnimation { velocity: 200 } } } } } // // TITLE BAR END // Rectangle { z: 999; id: processingSnapshot; anchors.fill: parent; visible: root.processing360Snapshot || !root.uiReady; color: Qt.rgba(0.0, 0.0, 0.0, 0.85); // This object is always used in a popup. // This MouseArea is used to prevent a user from being // able to click on a button/mouseArea underneath the popup/section. MouseArea { anchors.fill: parent; hoverEnabled: true; propagateComposedEvents: false; } AnimatedImage { id: processingImage; source: "processing.gif" width: 74; height: width; anchors.verticalCenter: parent.verticalCenter; anchors.horizontalCenter: parent.horizontalCenter; } HifiStylesUit.RalewaySemiBold { text: root.uiReady ? "Processing..." : ""; // Anchors anchors.top: processingImage.bottom; anchors.topMargin: 4; anchors.horizontalCenter: parent.horizontalCenter; width: paintedWidth; // Text size size: 26; // Style color: hifi.colors.white; verticalAlignment: Text.AlignVCenter; } } // // SPECTATOR CONTROLS START // Item { id: spectatorControlsContainer; // Anchors anchors.top: titleBarContainer.bottom; anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: parent.bottom; // Instructions or Preview Rectangle { id: spectatorCameraImageContainer; anchors.left: parent.left; anchors.top: parent.top; anchors.right: parent.right; height: 250; color: masterSwitch.checked ? "transparent" : "black"; AnimatedImage { source: "static.gif" visible: !masterSwitch.checked; anchors.fill: parent; opacity: 0.15; } // Instructions (visible when display texture isn't set) HifiStylesUit.FiraSansRegular { id: spectatorCameraInstructions; text: "Turn on Spectator Camera for a preview\nof " + (HMD.active ? "what your monitor shows." : "the camera's view."); size: 16; color: hifi.colors.white; visible: !masterSwitch.checked; anchors.fill: parent; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; } HifiStylesUit.FiraSansRegular { text: ":)"; size: 28; color: hifi.colors.white; visible: root.processing360Snapshot || root.processingStillSnapshot; anchors.fill: parent; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; } // Spectator Camera Preview Hifi.ResourceImageItem { id: spectatorCameraPreview; visible: masterSwitch.checked && !root.processing360Snapshot && !root.processingStillSnapshot; url: showCameraView.checked || !HMD.active ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame"; ready: masterSwitch.checked; mirrorVertically: true; anchors.fill: parent; onVisibleChanged: { ready = masterSwitch.checked; update(); } } Item { visible: HMD.active; anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; height: 40; LinearGradient { anchors.fill: parent; start: Qt.point(0, 0); end: Qt.point(0, height); gradient: Gradient { GradientStop { position: 0.0; color: hifi.colors.black } GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0) } } } HifiStylesUit.HiFiGlyphs { id: monitorShowsSwitchLabelGlyph; text: hifi.glyphs.screen; size: 32; color: hifi.colors.white; anchors.top: parent.top; anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.leftMargin: 16; } HifiStylesUit.RalewayLight { id: monitorShowsSwitchLabel; text: "Monitor View:"; anchors.top: parent.top; anchors.bottom: parent.bottom; anchors.left: monitorShowsSwitchLabelGlyph.right; anchors.leftMargin: 8; size: 20; width: paintedWidth; height: parent.height; color: hifi.colors.white; verticalAlignment: Text.AlignVCenter; } Item { anchors.left: monitorShowsSwitchLabel.right; anchors.leftMargin: 14; anchors.right: parent.right; anchors.rightMargin: 10; anchors.top: parent.top; anchors.bottom: parent.bottom; HifiControlsUit.RadioButton { id: showCameraView; text: "Camera View"; width: 125; anchors.left: parent.left; anchors.leftMargin: 10; anchors.verticalCenter: parent.verticalCenter; colorScheme: hifi.colorSchemes.dark; onClicked: { if (showHmdPreview.checked) { showHmdPreview.checked = false; } if (!showCameraView.checked && !showHmdPreview.checked) { showCameraView.checked = true; } } onCheckedChanged: { if (checked) { sendToScript({method: 'setMonitorShowsCameraView', params: true}); } } } HifiControlsUit.RadioButton { id: showHmdPreview; text: "VR Preview"; anchors.left: showCameraView.right; anchors.leftMargin: 10; width: 125; anchors.verticalCenter: parent.verticalCenter; colorScheme: hifi.colorSchemes.dark; onClicked: { if (showCameraView.checked) { showCameraView.checked = false; } if (!showCameraView.checked && !showHmdPreview.checked) { showHmdPreview.checked = true; } } onCheckedChanged: { if (checked) { sendToScript({method: 'setMonitorShowsCameraView', params: false}); } } } } } HifiStylesUit.HiFiGlyphs { id: flashGlyph; visible: flashCheckBox.visible; text: hifi.glyphs.lightning; size: 26; color: hifi.colors.white; anchors.verticalCenter: flashCheckBox.verticalCenter; anchors.right: flashCheckBox.left; anchors.rightMargin: -2; } HifiControlsUit.CheckBox { id: flashCheckBox; visible: masterSwitch.checked; color: hifi.colors.white; colorScheme: hifi.colorSchemes.dark; anchors.right: takeSnapshotButton.left; anchors.rightMargin: -8; anchors.verticalCenter: takeSnapshotButton.verticalCenter; boxSize: 22; onClicked: { sendToScript({method: 'setFlashStatus', enabled: checked}); } } HifiControlsUit.Button { id: takeSnapshotButton; enabled: masterSwitch.checked; text: "SNAP PICTURE"; colorScheme: hifi.colorSchemes.light; color: hifi.buttons.white; anchors.bottom: parent.bottom; anchors.bottomMargin: 8; anchors.right: take360SnapshotButton.left; anchors.rightMargin: 12; width: 135; height: 35; onClicked: { root.processingStillSnapshot = true; sendToScript({method: 'takeSecondaryCameraSnapshot'}); } } HifiControlsUit.Button { id: take360SnapshotButton; enabled: masterSwitch.checked; text: "SNAP 360"; colorScheme: hifi.colorSchemes.light; color: hifi.buttons.white; anchors.bottom: parent.bottom; anchors.bottomMargin: 8; anchors.right: parent.right; anchors.rightMargin: 12; width: 135; height: 35; onClicked: { root.processing360Snapshot = true; sendToScript({method: 'takeSecondaryCamera360Snapshot'}); } } } Item { anchors.top: spectatorCameraImageContainer.bottom; anchors.topMargin: 8; anchors.left: parent.left; anchors.leftMargin: 26; anchors.right: parent.right; anchors.rightMargin: 26; anchors.bottom: parent.bottom; Item { id: fieldOfView; visible: masterSwitch.checked; anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; height: 35; HifiStylesUit.RalewaySemiBold { id: fieldOfViewLabel; text: "Field of View (" + fieldOfViewSlider.value + "\u00B0): "; size: 20; color: hifi.colors.white; anchors.left: parent.left; anchors.top: parent.top; anchors.bottom: parent.bottom; width: 172; horizontalAlignment: Text.AlignLeft; verticalAlignment: Text.AlignVCenter; } HifiControlsUit.Slider { id: fieldOfViewSlider; anchors.top: parent.top; anchors.bottom: parent.bottom; anchors.right: resetvFoV.left; anchors.rightMargin: 8; anchors.left: fieldOfViewLabel.right; anchors.leftMargin: 8; colorScheme: hifi.colorSchemes.dark; from: 10.0; to: 120.0; value: 45.0; stepSize: 1; onValueChanged: { sendToScript({method: 'updateCameravFoV', vFoV: value}); } onPressedChanged: { if (!pressed) { sendToScript({method: 'updateCameravFoV', vFoV: value}); } } } HifiControlsUit.GlyphButton { id: resetvFoV; anchors.verticalCenter: parent.verticalCenter; anchors.right: parent.right; anchors.rightMargin: -8; height: parent.height - 8; width: height; glyph: hifi.glyphs.reload; onClicked: { fieldOfViewSlider.value = 45.0; } } } //------------------------------------------------------------------------------------------ Item { id: toneMap; visible: masterSwitch.checked; anchors.top: fieldOfView.bottom; anchors.topMargin: 18; anchors.left: parent.left; anchors.right: parent.right; height: 35; HifiStylesUit.RalewaySemiBold { id: toneMapLabel; text: "Tone mapping curve: " + toneMapSlider.value; size: 20; color: hifi.colors.white; anchors.left: parent.left; anchors.top: parent.top; anchors.bottom: parent.bottom; width: 220; horizontalAlignment: Text.AlignLeft; verticalAlignment: Text.AlignVCenter; } HifiControlsUit.Slider { id: toneMapSlider; anchors.top: parent.top; anchors.bottom: parent.bottom; anchors.right: resetToneMap.left; anchors.rightMargin: 8; anchors.left: toneMapLabel.right; anchors.leftMargin: 8; colorScheme: hifi.colorSchemes.dark; from: 0.0; to: 3.0; value: 1.0; stepSize: 1; onValueChanged: { sendToScript({method: 'updateToneMap', toneCurve: value}); } onPressedChanged: { if (!pressed) { sendToScript({method: 'updateToneMap', toneCurve: value}); } } } HifiControlsUit.GlyphButton { id: resetToneMap; anchors.verticalCenter: parent.verticalCenter; anchors.right: parent.right; anchors.rightMargin: -8; height: parent.height - 8; width: height; glyph: hifi.glyphs.reload; onClicked: { toneMapSlider.value = 1.0; } } } //---------------------------------------------------------------------------------------- Item { visible: HMD.active; anchors.top: toneMap.bottom; anchors.topMargin: 18; anchors.left: parent.left; anchors.right: parent.right; height: childrenRect.height; HifiStylesUit.RalewaySemiBold { id: shortcutsHeaderText; anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right; height: paintedHeight; text: "Shortcuts"; size: 20; color: hifi.colors.white; } // "Switch View From Controller" Checkbox HifiControlsUit.CheckBox { id: switchViewFromControllerCheckBox; color: hifi.colors.white; colorScheme: hifi.colorSchemes.dark; anchors.left: parent.left; anchors.top: shortcutsHeaderText.bottom; anchors.topMargin: 8; text: ""; labelFontSize: 20; labelFontWeight: Font.Normal; boxSize: 24; onClicked: { sendToScript({method: 'changeSwitchViewFromControllerPreference', params: checked}); } } // "Take Snapshot" Checkbox HifiControlsUit.CheckBox { id: takeSnapshotFromControllerCheckBox; color: hifi.colors.white; colorScheme: hifi.colorSchemes.dark; anchors.left: parent.left; anchors.top: switchViewFromControllerCheckBox.bottom; anchors.topMargin: 4; text: ""; labelFontSize: 20; labelFontWeight: Font.Normal; boxSize: 24; onClicked: { sendToScript({method: 'changeTakeSnapshotFromControllerPreference', params: checked}); } } } HifiControlsUit.Button { text: "Change Snapshot Location"; colorScheme: hifi.colorSchemes.dark; color: hifi.buttons.none; anchors.bottom: spectatorDescriptionContainer.top; anchors.bottomMargin: 16; anchors.left: parent.left; anchors.right: parent.right; height: 35; onClicked: { sendToScript({method: 'openSettings'}); } } Item { id: spectatorDescriptionContainer; // Size height: childrenRect.height; // Anchors anchors.left: parent.left; anchors.right: parent.right; anchors.bottom: parent.bottom; anchors.bottomMargin: 20; // "Spectator" app description text HifiStylesUit.RalewayRegular { id: spectatorDescriptionText; text: "While you're using a VR headset, you can use this app to change what your monitor shows. " + "Try it when streaming or recording video."; // Text size size: 20; // Size height: paintedHeight; // Anchors anchors.left: parent.left; anchors.right: parent.right; anchors.top: parent.top; // Style color: hifi.colors.white; wrapMode: Text.Wrap; // Alignment horizontalAlignment: Text.AlignHLeft; verticalAlignment: Text.AlignVCenter; } // "Learn More" text HifiStylesUit.RalewayRegular { id: spectatorLearnMoreText; text: "Learn More About Spectator"; // Text size size: 20; // Size width: paintedWidth; height: paintedHeight; // Anchors anchors.top: spectatorDescriptionText.bottom; anchors.topMargin: 10; anchors.left: parent.left; anchors.right: parent.right; // Style color: hifi.colors.blueAccent; wrapMode: Text.WordWrap; font.underline: true; // Alignment horizontalAlignment: Text.AlignHLeft; verticalAlignment: Text.AlignVCenter; MouseArea { anchors.fill: parent; hoverEnabled: enabled; onClicked: { letterbox(hifi.glyphs.question, "Spectator Camera", "By default, your monitor shows a preview of what you're seeing in VR. " + "Using the Spectator Camera app, your monitor can display the view " + "from a virtual hand-held camera - perfect for taking selfies or filming " + "your friends!
" + "

Streaming and Recording

" + "We recommend OBS for streaming and recording the contents of your monitor to services like " + "Twitch, YouTube Live, and Facebook Live.

" + "To get started using OBS, click this link now. The page will open in an external browser:
" + 'OBS Official Overview Guide

' + 'Snapshots taken using Spectator Camera will be saved in your Snapshots Directory - change via Settings -> General.'); } onEntered: parent.color = hifi.colors.blueHighlight; onExited: parent.color = hifi.colors.blueAccent; } } } } } // // SPECTATOR CONTROLS END // // // FUNCTION DEFINITIONS START // // // Function Name: fromScript() // // Relevant Variables: // None // // Arguments: // message: The message sent from the SpectatorCamera JavaScript. // Messages are in format "{method, params}", like json-rpc. // // Description: // Called when a message is received from spectatorCamera.js. // function fromScript(message) { switch (message.method) { case 'initializeUI': masterSwitch.checked = message.masterSwitchOn; flashCheckBox.checked = message.flashCheckboxChecked; showCameraView.checked = message.monitorShowsCamView; showHmdPreview.checked = !message.monitorShowsCamView; root.uiReady = true; break; case 'updateMonitorShowsSwitch': showCameraView.checked = message.params; showHmdPreview.checked = !message.params; break; case 'updateControllerMappingCheckbox': switchViewFromControllerCheckBox.checked = message.switchViewSetting; switchViewFromControllerCheckBox.enabled = true; takeSnapshotFromControllerCheckBox.checked = message.takeSnapshotSetting; takeSnapshotFromControllerCheckBox.enabled = true; if (message.controller === "OculusTouch") { switchViewFromControllerCheckBox.text = "Left Thumbstick: Switch Monitor View"; takeSnapshotFromControllerCheckBox.text = "Right Thumbstick: Take Snapshot"; } else if (message.controller === "Vive") { switchViewFromControllerCheckBox.text = "Left Thumb Pad: Switch Monitor View"; takeSnapshotFromControllerCheckBox.text = "Right Thumb Pad: Take Snapshot"; } else { switchViewFromControllerCheckBox.text = "Pressing Ctrl+0 Switches Monitor View"; switchViewFromControllerCheckBox.checked = true; switchViewFromControllerCheckBox.enabled = false; takeSnapshotFromControllerCheckBox.visible = false; } break; case 'finishedProcessing360Snapshot': root.processing360Snapshot = false; break; case 'startedProcessingStillSnapshot': root.processingStillSnapshot = true; break; case 'finishedProcessingStillSnapshot': root.processingStillSnapshot = false; break; default: console.log('Unrecognized message from spectatorCamera.js:', JSON.stringify(message)); } } signal sendToScript(var message); // // FUNCTION DEFINITIONS END // }