Learn how to use the VRED Core Web API.
Video Captions: Hello and welcome to our last tutorial video for VRED Core. I'm your host Christopher and today we will extend our streaming app example from the last video and implement more features with VRED's Web API.
With the Python API v2, Autodesk also introduced a Python Web API, that is a direct successor to the Python endpoints you can use to send Python commands to VRED. We used Python endpoints in our previous video, where you build our own HTML interface for VRED Core, to send Python commands to VRED and implement our video stream.
For example, we can send arbitrary Python commands to the Python endpoint and receive the result of this operation as a response. The disadvantage of these endpoints is that you have to send a string representation of your Python commands, instead of using object or rendered interfaces, and that you have to properly pass the return values in order to use them in your javascript app.
With the new Web API, VRED offers a fully-integrated javascript module that acts as a direct connection to VRED's Python interfaces. The WebAPI offers an object-oriented access point to all functionality that is available with the Python API v2. That means that you have access to proper javascript types and can use typical javascript features, like promises or error handling.
For example, you can create a new camera with a javascript call like that and awake the camera that you just created. You can then use that camera object to set its name, set its active, or processes in another way. The camera object, of course, contains all functions that would also be available in the Python class, vrdCamera
.
Let's see how we can use the new Web API in our own app. In this tutorial, we will integrate an annotation manager on top of our custom HTML app from the previous video. If you haven't seen the previous video, I recommend watching it first because it will directly extend this code of this tutorial.
OK. Let's start.
In the last tutorial, we put all the CSS and javascript code in the HTML file. To keep it tidy, I moved the stylesheet and script in separate files.In this way, our HTML is much cleaner.
At first, I want to add a way to add annotations to our scene. For this tutorial, we don't want to set the location of these annotations, just add them and give them a name and a description. To do this, we can add a form to our HTML code that contains 2 inputs for the name and the description, and a Submit button to create the annotation. We can wrap this in a div container, give it some styles with CSS, and place it over our video stream. This time, in the upper left corner.
Next, we create a new javascript file called, webapiscript
, that will contain our javascript code for our annotation manager. The javascript API for VRED Core can be added to a javascript module with this import statement. As you see, I'm using localhost
with the port 8888
for my example. If your VRED Core setting is set up differently, then you have to adjust these values.
At first, I'm adding a new function, addAnnotation
, with the parameters name
and description
. In the function, I directly call VRED's javascript API that we imported. As you can see, we can use the API very similar to the Python API.
Here, I'm using the vrAnnotation
servers and call its method, createAnnotation
with the name
parameter. This will return a promise object that we handle asynchronously with the then
function. If you are not familiar with the javascript pattern, it's called async/await pattern and it is a modern approach to deal with concurrency in programming. createAnnotation
basically needs some time before it can return, so we wait for it, and then do the next action. In this way, the script can then just continue working and does not look up when we wait for VRED.
Inside of the and then
function, we now can safely call the findAnnotation
function to get a reference to the new annotation node. We follow the same async/await pattern to set the text of the new annotation to our description
parameter. If there is an error while creating the annotation, we can catch this error and notify the user. In this case, I will just print an error message to a Python terminal.
So, adding annotations should work now, but we still have to connect this to our HTML form we created. To do this, we hook on to the unsubmit callback of the form. Here, we can extract a name and description inputs and call the addAnnotation
function. When we test our new feature, we can see that everything is working fine and that we can add new annotations to our scene. Not bad!
I also want to add a list of annotations in our scene with the ability to delete annotations. Now in our HTML, we just have to add an unordered list element with an ID. This is enough to dynamically add and remove annotations to this list.
In our javascript script, we start with a function that will load all annotations. Use the vrAnnotationService
to get a list of all annotations. With the async/await pattern, wait for a list of all annotations from VRED, and then iterate over them, one-by-one.
For each annotation, we queried a name and containing text. This is also done with the async/await pattern. That's why we have to mark our loop
function as async
. With the name and the annotation text, we can call a second function called, addAnnotationItem
, that we will implement in a moment.
But first, we want to clear our list whenever the loadAnnotation
function is called. For this, we add a deleteAnnotationItems
function that clears all items from all lists and call it at the beginning of our load
function. And, then we create a new function called, addAnnotationItem
with the parameter name
and description
.
Here we create a new HTML list element that we fill with a text node that will hold the name and the description of the annotation. If the name is undefined, we just return from this function.
We also want to add a button that will delete our annotation in VRED. To do this, we can add a link to our annotation. We need to set something as the link attribute, in order to see the link. But, this has no functionality. Instead, we hook onto the onClick
function and use the AnnotationService
to delete this annotation, if it is clicked.
We also use the async/await pattern here. At the end, we append our annotation element to the list. Right now, the annotation list would load once when we load the streaming app, but it would never update. To make our app really dynamic, we can use the signals from the vrAnnotationService
that tell us when an annotation was added or deleted. This is very similar to using signals with the Python interface.
That's it. When we test the app, we can add new annotations and immediately see them in our annotations list. We also have a Delete button that will remove the annotation from our scene.
I admit, all annotations are added at the same location, but this could be a task for another tutorial. You should now have a pretty good idea about working with VRED's Web API, and how you can use it to build dynamic and interactive HTML applications to control your scenes. This video also concludes our tutorial series for VRED Core. I really hope I could help you getting started with VRED Core, and give you an idea about the possibilities you have in extending and using this product.
Thanks for joining me and see you next time.
Here are the accompanying example Python scripts for the Tutorial 6: How to use the VRED Core Web API video.
To download these files zipped, click here.
<!DOCTYPE html>
<html>
<head>
<title>Web API Example: Create Camera</title>
Web API Module
<script type="module">
import { api } from 'http://localhost:8888/api.js';
window.onload = function() {
// Use vrCameraService to create a camera in the scene
// Wait for the result and "then"...
api.vrCameraService.createCamera("New Custom Camera").then(async (vrdCamera) => {
// ... set a new name for the camera...
await vrdCamera.setName("New Custom Camera - Changed Name");
// ... and set the camera active
await vrdCamera.activate();
// Test
api.vrCameraService.getCameraNames().then((names) => {
window.alert(names.join('\n'));
})
});
}
</script>
</head>
</html>
<body>
</body>
<!DOCTYPE html>
<html>
<head>
<title>My Custom App</title>
<meta charset="utf-8"/>
<link rel="stylesheet" href="styles.css">
<script src="script.js"></script>
<script type="module" src="web-api-script.js"></script>
</head>
<body>
<div id="page">
<div id="add-annotations-container">
<h2>Annotations</h2>
<form id="annotationForm">
<input name="name" type="text" placeholder="Name" required />
<input name="description" type="text" placeholder="Description" required />
<button type="submit">Add</button>
</form>
<ul id="annotations"></ul>
</div>
Variant Set Container
<div id="variant-set-container">
<h2>Variant Sets</h2>
</div>
Video Stream Container
<iframe id="stream-container" frameborder="0" overflow="hidden" scroll="no"></iframe>
Python Terminal Container
<div class="python-interface">
<h2>Python</h2>
<div class="python-send-wrapper">
<input id="python-value" type="text" placeholder="Enter your python code..."></input>
<button onclick="executeTerminalCode(document.getElementById('python-value').value)">Send Python</button>
</div>
<textarea id="python-response"></textarea>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>My Custom App</title>
<meta charset="utf-8"/>
<link rel="stylesheet" href="styles.css">
<script src="script.js"></script>
</head>
<body>
<div id="page">
Variant Set Container
<div id="variant-set-container"></div>
Video Stream Container
<iframe id="stream-container" frameborder="0" overflow="hidden" scroll="no"></iframe>
Python Terminal Container
<div class="python-interface">
<div class="python-send-wrapper">
<input id="python-value" type="text" placeholder="Enter your python code..."></input>
<button onclick="executeTerminalCode(document.getElementById('python-value').value)">Send Python</button>
</div>
<textarea id="python-response"></textarea>
</div>
</div>
</body>
</html>
var host = "localhost";
var port = "8888";
function initializeStream() {
var w = window.innerWidth;
var h = window.innerHeight;
document.getElementById("stream-container").src = 'http://' + host + ':' + port + '/apps/VREDStream/index.html?width='+w+'&height='+h+'';
}
function updateVariantSets() {
sendAndReceivePython("getVariantSets()",
(response) => {
var variantSets = eval(response);
if(variantSets) {
// get a reference to our variant set container
var variantSetContainer = document.getElementById('variant-set-container');
// add element for each variant set
variantSets.forEach(variantSet => {
var variantSetNode = document.createElement('div');
variantSetNode.innerHTML = variantSet;
variantSetNode.onclick = function() {
sendAndReceivePython("selectVariantSet('"+variantSet+"')");
}
variantSetContainer.appendChild(variantSetNode);
});
}
}
);
}
function executeTerminalCode(python) {
console.log(python);
sendAndReceivePython(python,
(response) => {
var text = document.getElementById('python-response').value;
text += '\n' + response;
document.getElementById('python-response').value = text;
},
(error) => console.error("Error sending pyhton command.", python)
);
}
function sendAndReceivePython(command, successCallback, errorCallback) {
var http = new XMLHttpRequest();
var url = 'http://' + host + ':' + port + '/pythoneval2?value=' + encodeURI(command);
http.open('GET', url, true);
http.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if(this.responseText && successCallback) {
successCallback(this.responseText)
}
}
};
http.onerror = function() {
if(errorCallback) {
errorCallback();
}
}
http.send();
}
window.onload = function() {
initializeStream();
updateVariantSets();
}
window.onresize = function() {
initializeStream();
}
html, body {
padding: 0;
margin: 0;
width: 100vw;
height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif
}
h2 {
color: white;
}
#page {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
#variant-set-container {
padding: 1em 2em;
position: absolute;
left: 1em;
bottom: 1em;
background-color: #2d2d2d;
display: flex;
flex-direction: column;
height: 20em;
overflow-y: scroll;
color: white;
}
#variant-set-container > div {
padding: 0.1em 0.2em;
background-color: #4b4b4b;
margin-bottom: 0.1em;
cursor: pointer;
}
#stream-container {
width: 100%;
height: 100%;
overflow: hidden;
}
.python-interface {
position: absolute;
padding: 1em 2em;
right: 1em;
bottom: 1em;
background-color: #2d2d2d;
display: flex;
flex-direction: column;
}
.python-send-wrapper {
margin-bottom: 0.5em;
}
.python-send-wrapper > input {
width: 25em;
}
textarea#python-response {
height: 10em;
resize: none;
}
#add-annotations-container {
position: absolute;
padding: 1em 2em;
left: 1em;
top: 1em;
background-color: #2d2d2d;
display: flex;
flex-direction: column;
}
#annotations {
color: white;
}
#annotations li {
list-style: none;
display: flex;
}
#annotations li strong {
margin-right: 0.5em;
}
#annotations li a {
color: white;
margin-left: 0.5em;
float: right;
}
import { api } from "http://localhost:8888/api.js";
// -- PART 1 --
// Add an annotation with a name and a description
function addAnnotation(name, description) {
api.vrAnnotationService
.createAnnotation(name)
.then(() => {
api.vrAnnotationService.findAnnotation(name).then((a) => {
a.setText(description);
addEventMessage("Annotations added successfully");
});
})
.catch(() =>
document.getElementById("python-response")("Add annotation failed")
);
}
// Annotation form
annotationForm.onsubmit = (ev) => {
ev.preventDefault();
const name = ev.target.elements.name.value;
const description = ev.target.elements.description.value;
addAnnotation(name, description);
};
// -- END: PART 1 --
function addEventMessage(message) {
var textarea = document.getElementById("python-response");
var text = textarea.value;
textarea.value = message + "\n" + text;
}
// Adds a new list item to the unordered list #annotations.
// function addAnnotationItem(name, description, deleteCallback) {
function addAnnotationItem(name, description) {
let item = undefined;
if (name) {
item = document.createElement("li");
const nameItem = document.createElement("strong");
nameItem.appendChild(document.createTextNode(name));
item.appendChild(nameItem);
} else {
return;
}
if (description) {
const descriptionItem = document.createElement("span");
descriptionItem.appendChild(document.createTextNode(description));
item.appendChild(descriptionItem);
}
const link = document.createElement("a");
link.href = `?delete=${name}`;
link.appendChild(document.createTextNode("( X )"));
link.onclick = (e) => {
deleteAnnotation(name);
e.preventDefault();
};
item.appendChild(link);
document.getElementById("annotations").appendChild(item);
}
function deleteAnnotation(name) {
api.vrAnnotationService
.findAnnotation(name)
.then((a) => api.vrAnnotationService.deleteAnnotation(a));
}
// Delete all annotations in the list
function deleteAnnotationItems() {
const list = document.getElementById("annotations");
list.textContent = "";
}
// -- PART 2.2 --
// Loads annotations and adds annotation names to the unordered list #annotations.
function loadAnnotations() {
deleteAnnotationItems();
api.vrAnnotationService
.getAnnotations()
.then((annotations) => {
if (annotations.length > 0) {
annotations.forEach(async (a) => {
const name = await a.getName();
const description = await a.getText();
addAnnotationItem(name, description);
});
} else {
addAnnotationItem("No annotations found.");
}
})
.then(() => addEventMessage("Annotations loaded successfully"))
.catch(() => addEventMessage("Loading annotations failed"));
}
// -- END: PART 2.2 --
// -- PART 2.1 --
// Add event listeners
api.vrAnnotationService.annotationsAdded.connect(() => {
loadAnnotations();
});
api.vrAnnotationService.annotationsDeleted.connect(() => {
loadAnnotations();
});
// Load annotations
loadAnnotations();
// -- END: PART 2.1 --