Serving a frontend site

Up to this point, we have created the backend capabilities of our sample Service on the remote site. What if we now also wished to expose our viewport capture feature to the outside, in a User-friendly way?

Using the API we exposed in earlier steps, we could choose to integrate calls to our REST Service by building an Omni.UI interface and exposing it via a custom Omniverse application, but what if we wished to make it available to multiple Users at the same time? What if these Users are using devices for which Omniverse is not available, or if the installation costs are prohibitive?

In this case, serving a static website via an Omniverse extension might be the most straightforward way to make our feature available to many, and may also serve as a guide for you to expose additional Services you may have built to other applications on your network.

This is the experience we will be creating:

Omniverse Services frontend sample

Note

You may already be aware that there are quite a number of front-end frameworks available to assist with the creation of rich web experiences. In the scope of this minimalistic tutorial, we will be using a pure JavaScript implementation for our application logic in order not to steer you in a direction that may not match the tooling you may already be familiar with, or to replace a course on front-end web development.

As our goal is to get you up and running with the maximum amount of knowledge in the minimum amount of time, the front-end code samples we will be sharing here should all be accessible to you without requiring any additional tooling other than a text editor and a web browser.

In case you may be interested to learn more about potential framework options, know that React, Svelte, Vue and others are some of the most popular and feature-rich ones, supported by a strong community building open-source components, creating learning material and offering support. Feel free to explore any of these options and pick the one most suited for your situation.

Initial implementation

Let’s define a new extension, which will depend on the one exposing the API that we created a moment ago. This new extension will only serve the static resources responsible for implementing our website, which are:

  • An HTML file, containing the basic structure of our microsite.

  • A JavaScript file, containing the application logic of our microsite, responsible for handling the User interactions and network communication with our Service.

  • A CSS file, containing the styles of our microsite.

As a side-note, you may be wondering why we are opting to create a new extension to serve this frontend UI for the Service we defined earlier. While there are no technical limitations preventing us from exposing both the APIs and the web resources from the same Omniverse extension, the main motivation for offering these features as distinct extensions is to maintain a clear separation of concerns between these components. Some of the benefits of this approach include:

  • Encouraging healthy version management practices, by documenting API changes and ensuring backwards compatibility by separating the data access layer from the UI, thus ensuring that API changes do not break existing client implementations that may be used in the wild. In other words, enforcing any API changes to be mindful of existing customers.

  • Encouraging parallelization of work, by decoupling the API components from the UI, and making it easier for backend engineers to implement server-side features while frontend engineers refine the User experience, without both groups stepping on each others’ toes while iterating on their respective tasks.

  • Allowing piecemeal deployment of features, by only exposing components that are needed for the experience. This makes it possible to only expose the API of our Service without any web UI if needed in cases where this feature would not be required, thus not exposing this web component if not needed.

  • Limiting the impact of potential future expansion, by limiting the side-effects of eventual changes planned for the future. For example, an Omni.UI widget for accessing the features exposed by our Service could be developed as part of another extension to expose UI controls within an Omniverse application, without overlapping with the features already implemented thus far.

  • Keeping the build system for extensions simple and efficient, by limiting the impact of build tools and integrations on build times and configurations. As an illustrative example, backend contributors may find it preferable not to transpile TypeScript resources used for the frontend when performing maintenance on the data access layer.

Extension manifest

With this out of the way, let’s start with our new extension’s main file:

exts/omni.services.example.viewport_capture.ui/config/extension.toml
 1[package]
 2# Semantic versioning is used: https://semver.org
 3version = "1.0.0"
 4
 5# The title and description fields are primarily for displaying extension info in UI:
 6title = "Viewport capture service UI"
 7description = "Sample service example demonstrating hosting microsites using Omniverse."
 8
 9# Path (relative to the root), or content of the extension's "readme" file, in Markdown format:
10readme = "docs/README.md"
11
12# Path (relative to the root) of the extension's changelog file, in Markdown format:
13changelog = "docs/CHANGELOG.md"
14
15# URL of the extension source repository:
16repository = "https://github.com/NVIDIA-Omniverse/kit-extension-template"
17
18# Category of the extension:
19category = "services"
20
21# Keywords of the extension:
22keywords = ["kit", "service", "example"]
23
24# Icon to show in the extension manager:
25icon = "data/icon.png"
26
27# Preview to show in the extension manager:
28preview_image = "data/preview.png"
29
30[dependencies]
31"omni.services.core" = {}
32"omni.services.transport.server.http" = {}
33# Define a dependency on the extension serving the API, which we created earlier:
34"omni.services.example.viewport_capture.core" = {}
35
36[[python.module]]
37name = "omni.services.example.viewport_capture.ui"
38
39[settings]
40exts."omni.services.transport.server.http".http.enabled = true
41exts."omni.services.transport.server.http".host = "0.0.0.0"
42exts."omni.services.transport.server.http".port = 8011
43
44# Settings of our extension:
45[settings.exts."omni.services.example.viewport_capture.ui"]
46# URL location where our microsite will be available for Users via a web browser:
47site_path = "/viewport-capture"
48
49# Name of the folder containing static web resources (HTML, CSS, JavaScript, etc.):
50web_resources = "web"

Extension entrypoint

Let’s now define our extension, which only consists of “mounting” our static web resources from their location on the host, and mapping this location to a URL path. This will make it possible for Users to navigate to our microsite by typing its URL in a web browser, which will serve them the experience we will be implementing in HTML and JavaScript in just a moment:

exts/omni.services.example.viewport_capture.ui/omni/services/example/viewport_capture/ui/python_ext.py
 1import os
 2
 3from fastapi.staticfiles import StaticFiles
 4
 5import carb
 6
 7import omni.ext
 8from omni.services.core.main import deregister_mount, register_mount
 9
10
11def get_web_resources_path(ext_id: str) -> str:
12    """
13    Utility method to return the location of the folder where the static HTML resources for the site we are hosting are located on the host.
14
15    Args:
16        ext_id (str): Unique identifier of the extension containing the static resources of the site.
17
18    Return:
19        str: The location on the host where the static resources of the site are located.
20
21    """
22    ext_name = ext_id.split("-")[0]
23
24    extension_path = omni.kit.app.get_app_interface().get_extension_manager().get_extension_path(ext_id)
25    web_resources = carb.settings.get_settings().get_as_string(f"exts/{ext_name}/web_resources")
26
27    return os.path.join(extension_path, web_resources)
28
29
30# Any class derived from `omni.ext.IExt` in the top level module (defined in the `python.module` section of the
31# `extension.toml` file) will be instantiated when the extension is enabled, and its `on_startup(ext_id)` method
32# will be called. When disabled or when the application is shut down, its `on_shutdown()` will be called.
33class ViewportCaptureUIExtension(omni.ext.IExt):
34    """Sample extension illustrating registration of web resources for a frontend microsite."""
35
36    # `ext_id` is the unique identifier of the extension, containing its name and semantic version number. This
37    # identifier can be used in conjunction with the Extension Manager to query for additional information, such
38    # as the extension's location on the filesystem.
39    def on_startup(self, ext_id: str) -> None:
40        ext_name = ext_id.split("-")[0]
41
42        # Extract the URL path where the site will be accessible on the host:
43        self._site_path = carb.settings.get_settings().get_as_string(f"exts/{ext_name}/site_path")
44
45        # Register this location as a mount, so its content is served by the web server bundled with the Omniverse
46        # application instance, thus making the site available on the network:
47        web_resources_path = get_web_resources_path(ext_id=ext_id)
48        register_mount(
49            path=self._site_path,
50            # Indicate to the web server hosted within the Omniverse application that the folder containing the static
51            # files of our website contains static HTML files. This has the effect of having the server automatically
52            # serve Users the "index.html" file contained within this directory when navigating to the
53            # "http://localhost:<port>"/viewport-capture" URL, which would otherwise present a list of all the files
54            # present at that location:
55            app=StaticFiles(directory=web_resources_path, html=True),
56            name="viewport-capture-web-resources",
57        )
58
59    def on_shutdown(self) -> None:
60        # When disabling the extension or shutting down the instance of the Omniverse application, let's make sure we
61        # also deregister our Service's `mount` in order to avoid our API being erroneously hosted at the given location
62        # despite the extension function no longer being enabled:
63        deregister_mount(path=self._site_path)

Web resources

Now that we defined the behaviour of our extension, which will be responsible for serving the web resources of our microsite, let’s shift our focus towards this front-end experience.

As you may have seen in the extension’s definition right above, we will be serving these static web resources from a location in the extension’s directory on the server indicated by the get_web_resources_path(ext_id) function. Let’s make sure the build process of our extension correctly links to these files, to make sure they are included in any packages we may create when distributing our application at a later time, to ensure they will ultimately be included in Docker containers or ZIP archives:

exts/omni.services.example.viewport_capture.ui/premake5.lua
 1-- Use folder name to build extension name and tag. Version is specified explicitly.
 2local ext = get_current_extension_info()
 3
 4project_ext (ext)
 5
 6repo_build.prebuild_link {
 7    { "omni", ext.target_dir.."/omni" },
 8    { "data", ext.target_dir.."/data" },
 9    { "docs", ext.target_dir.."/docs" },
10    -- Indicate to the extension build system that the static web resources hosted in the "/web" folder should be
11    -- correctly linked to during the build process:
12    { "web", ext.target_dir.."/web" },
13}

HTML

Let’s now dive into our web application, by starting with the HTML file that will hold the references to the JavaScript file with the logic of our application, as well as the CSS file containing the style definitions of our experience:

Click to expand the HTML code...
exts/omni.services.example.viewport_capture.ui/web/index.html
 1<!doctype html>
 2<html lang="en" data-bs-theme="dark">
 3<head>
 4    <meta charset="utf-8">
 5    <meta name="viewport" content="width=device-width, initial-scale=1">
 6    <meta name="description" content="Omniverse Services - Frontend Example">
 7    <title>Omniverse Services &ndash; Frontend Example</title>
 8
 9    <!-- Import styles used for the application: -->
10    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
11    <link href="./style.css" rel="stylesheet">
12</head>
13<body>
14
15    <!-- Header of the page: -->
16    <header data-bs-theme="dark">
17        <div class="navbar navbar-dark bg-dark shadow-sm">
18            <div class="container">
19                <a href="#" class="navbar-brand d-flex align-items-center">
20                    <svg width="286" height="26" viewBox="0 0 286 26" fill="none" xmlns="http://www.w3.org/2000/svg" class="svelte-df2sxi"><path d="M140.061 20.8363V20.4478H140.312C140.449 20.4478 140.637 20.4579 140.637 20.624C140.637 20.8045 140.541 20.8363 140.378 20.8363H140.061V20.8363ZM140.061 21.1093H140.229L140.619 21.7896H141.047L140.616 21.0819C140.839 21.066 141.022 20.9605 141.022 20.6615C141.022 20.2918 140.765 20.1719 140.33 20.1719H139.7V21.7882H140.062V21.1079L140.061 21.1093ZM141.894 20.9808C141.894 20.0318 141.15 19.48 140.322 19.48C139.495 19.48 138.747 20.0303 138.747 20.9808C138.747 21.9312 139.489 22.483 140.322 22.483C141.156 22.483 141.894 21.9298 141.894 20.9808ZM141.44 20.9808C141.44 21.6726 140.928 22.1363 140.322 22.1363V22.132C139.7 22.1363 139.198 21.6726 139.198 20.9808C139.198 20.2889 139.701 19.8266 140.322 19.8266C140.944 19.8266 141.44 20.2889 141.44 20.9808Z" fill="white"></path><path d="M84.2106 4.92277V21.9614H89.0584V4.92277H84.2106V4.92277ZM46.0831 4.89966V21.9614H50.9731V8.71733L54.7878 8.73033C56.042 8.73033 56.9106 9.02933 57.5144 9.66921C58.2811 10.481 58.5939 11.7882 58.5939 14.1802V21.9614H63.3311V12.535C63.3311 5.80677 59.0115 4.89966 54.7864 4.89966H46.0831ZM92.0148 4.92277V21.96H99.8757C104.064 21.96 105.431 21.2681 106.91 19.7182C107.955 18.6291 108.63 16.24 108.63 13.6284C108.63 11.2335 108.058 9.09721 107.062 7.76688C105.266 5.38788 102.679 4.92277 98.8166 4.92277H92.0148V4.92277ZM96.8219 8.6321H98.9053C101.929 8.6321 103.884 9.97977 103.884 13.4768C103.884 16.9738 101.929 18.3229 98.9053 18.3229H96.8219V8.6321ZM77.2227 4.92277L73.178 18.4254L69.3021 4.92277H64.0702L69.6062 21.96H76.5912L82.1709 4.92277H77.2241H77.2227ZM110.885 21.96H115.733V4.92277H110.884V21.96H110.885ZM124.473 4.92855L117.704 21.9542H122.484L123.555 18.9454H131.564L132.578 21.9542H137.766L130.947 4.9271H124.473V4.92855ZM127.618 8.03555L130.554 16.0118H124.589L127.618 8.03555Z" fill="white"></path><path d="M14.8242 7.76243V5.41809C15.054 5.4022 15.2854 5.3892 15.5211 5.38198C21.9809 5.17976 26.2191 10.8925 26.2191 10.8925C26.2191 10.8925 21.6419 17.2048 16.7345 17.2048C16.0274 17.2048 15.3945 17.0921 14.8242 16.9014V9.79042C17.3397 10.0923 17.8446 11.1944 19.3562 13.6962L22.7185 10.881C22.7185 10.881 20.2641 7.68442 16.1263 7.68442C15.6767 7.68442 15.2461 7.7162 14.8227 7.76098L14.8242 7.76243ZM14.8227 0.0158691V3.51865C15.054 3.49987 15.2868 3.48543 15.5196 3.47676C24.5023 3.17631 30.3554 10.7914 30.3554 10.7914C30.3554 10.7914 23.6337 18.9063 16.6297 18.9063C15.9881 18.9063 15.3872 18.8471 14.8227 18.7489V20.9141C15.3057 20.9748 15.8062 21.0109 16.3271 21.0109C22.8437 21.0109 27.5576 17.7074 32.1217 13.7959C32.8782 14.3968 35.9758 15.86 36.613 16.5013C32.273 20.1081 22.1613 23.0143 16.4275 23.0143C15.8746 23.0143 15.3436 22.9811 14.8227 22.932V25.974H39.5927V0.0173139H14.8242L14.8227 0.0158691ZM14.8227 16.9V18.7489C8.79499 17.6814 7.12183 11.4616 7.12183 11.4616C7.12183 11.4616 10.0157 8.27809 14.8227 7.76243V9.79042C14.8227 9.79042 14.8169 9.79042 14.814 9.79042C12.2912 9.48998 10.3212 11.83 10.3212 11.83C10.3212 11.83 11.4255 15.769 14.8242 16.9029L14.8227 16.9ZM4.11888 11.1944C4.11888 11.1944 7.6907 5.9612 14.8242 5.41954V3.52154C6.92396 4.15131 0.0814819 10.7943 0.0814819 10.7943C0.0814819 10.7943 3.95593 21.9165 14.8242 22.9349V20.917C6.84831 19.9203 4.11888 11.1944 4.11888 11.1944V11.1944Z" fill="#74B71B"></path><path d="M147 22.0616V5.13851H153.75C155.46 5.13851 156.895 5.46891 158.056 6.12972C159.234 6.79053 160.121 7.71726 160.718 8.90994C161.331 10.1026 161.637 11.5048 161.637 13.1165V14.0594C161.637 16.5414 160.96 18.4997 159.605 19.9341C158.25 21.3524 156.298 22.0616 153.75 22.0616H147ZM150.532 19.0154H153.653C155.089 19.0154 156.185 18.6206 156.944 17.8308C157.718 17.025 158.105 15.792 158.105 14.1319V13.044C158.105 11.4484 157.718 10.2396 156.944 9.41763C156.185 8.59565 155.089 8.18466 153.653 8.18466H150.532V19.0154Z" fill="white"></path><path d="M164.909 22.0616V5.13851H176.159V8.13631H168.441V12.0286H174.707L174.296 15.0264H168.441V19.0638H176.304V22.0616H164.909Z" fill="white"></path><path d="M184.082 22.0616L177.792 5.13851H181.615L185.776 17.6132L189.937 5.13851H193.566L187.252 22.0616H184.082Z" fill="white"></path><path d="M195.859 22.0616V5.13851H207.109V8.13631H199.392V12.0286H205.658L205.246 15.0264H199.392V19.0638H207.255V22.0616H195.859Z" fill="white"></path><path d="M210.461 22.0616V5.13851H213.993V19.0154H221.372V22.0616H210.461Z" fill="white"></path><path d="M231.323 22.4C229.678 22.4 228.267 22.0616 227.09 21.3847C225.928 20.7077 225.033 19.7488 224.404 18.5077C223.791 17.2667 223.485 15.792 223.485 14.0836V13.1165C223.485 11.4081 223.791 9.93338 224.404 8.69236C225.033 7.45133 225.928 6.49236 227.09 5.81543C228.267 5.13851 229.678 4.80005 231.323 4.80005H231.565C233.21 4.80005 234.614 5.13851 235.775 5.81543C236.952 6.49236 237.848 7.45133 238.46 8.69236C239.089 9.93338 239.404 11.4081 239.404 13.1165V14.0836C239.404 15.792 239.089 17.2667 238.46 18.5077C237.848 19.7488 236.952 20.7077 235.775 21.3847C234.614 22.0616 233.21 22.4 231.565 22.4H231.323ZM231.348 19.3055H231.541C232.864 19.3055 233.912 18.8784 234.686 18.0242C235.477 17.17 235.872 15.8806 235.872 14.1561V13.044C235.872 11.3195 235.477 10.0301 234.686 9.17587C233.912 8.32166 232.864 7.89455 231.541 7.89455H231.348C230.025 7.89455 228.969 8.32166 228.178 9.17587C227.404 10.0301 227.017 11.3195 227.017 13.044V14.1561C227.017 15.8806 227.404 17.17 228.178 18.0242C228.969 18.8784 230.025 19.3055 231.348 19.3055Z" fill="white"></path><path d="M242.663 22.0616V5.13851H249.075C250.381 5.13851 251.51 5.36415 252.462 5.81543C253.429 6.2506 254.171 6.87917 254.687 7.70115C255.22 8.50701 255.486 9.4821 255.486 10.6264V11.0858C255.486 12.8425 254.905 14.2044 253.744 15.1715C252.583 16.1224 251.026 16.5979 249.075 16.5979H246.196V22.0616H242.663ZM246.196 13.5517H248.978C249.817 13.5517 250.518 13.3663 251.083 12.9957C251.663 12.625 251.954 12.0125 251.954 11.1583V10.5539C251.954 9.73192 251.663 9.13558 251.083 8.76488C250.518 8.37807 249.817 8.18466 248.978 8.18466H246.196V13.5517Z" fill="white"></path><path d="M258.044 22.0616V5.13851H269.294V8.13631H261.576V12.0286H267.842L267.431 15.0264H261.576V19.0638H269.439V22.0616H258.044Z" fill="white"></path><path d="M272.645 22.0616V5.13851H279.056C281.024 5.13851 282.581 5.59785 283.726 6.51653C284.887 7.43521 285.468 8.70847 285.468 10.3363V10.7957C285.468 11.94 285.185 12.907 284.621 13.6968C284.073 14.4865 283.298 15.0828 282.298 15.4858L286 22.0616H282.153L278.815 16.0176H276.177V22.0616H272.645ZM276.177 12.9715H278.96C279.815 12.9715 280.524 12.8103 281.089 12.488C281.653 12.1656 281.936 11.6257 281.936 10.8682V10.2638C281.936 9.53851 281.653 9.0147 281.089 8.69236C280.524 8.3539 279.815 8.18466 278.96 8.18466H276.177V12.9715Z" fill="white"></path></svg>
21                </a>
22            </div>
23        </div>
24    </header>
25
26    <!-- Main section of the page: -->
27    <main class="d-flex flex-column">
28        <section class="py-5 text-center container">
29            <div class="row py-lg-5">
30                <div class="col-lg-6 col-md-8 mx-auto">
31                    <h1 class="fw-light">Omniverse Service Example</h1>
32                    <p class="lead text-body-secondary">A minimalistic example showcasing communication between Omniverse Services and front-end frameworks to display viewport screen captures.</p>
33                    <p>
34                        <a href="https://docs.omniverse.nvidia.com/services/latest/tutorials/viewport-capture/index.html" target="_blank" class="btn btn-secondary my-2">Follow the tutorial</a>
35                    </p>
36                </div>
37            </div>
38        </section>
39
40        <div class="album py-5 bg-body-tertiary flex-grow-1">
41            <div class="container">
42                <div class="card">
43                    <div class="card-body">
44                        <h5 class="card-title mb-3">Viewport Capture Example</h5>
45                        <p class="card-text">Code sample demonstrating using an Omniverse Service to capture the application viewport after loading the USD stage at the given location. This simple scenario illustrates how information can be shared between applications, by exchanging messages between components distributed across the network.</p>
46                        <p class="card-text">To experiment with various scenarios, and become more familiar with best practices such as error handling, try capturing a USD Stage at a location which does not exist.</p>
47                        <p class="card-text">
48                            <div class="pt-3 mb-3">
49                                <label for="stage-location" class="form-label fw-bold">Location of the USD Stage to capture:</label>
50                                <input type="text" class="form-control" id="stage-location" placeholder="http://omniverse-content-production.s3-us-west-2.amazonaws.com/Samples/Astronaut/Astronaut.usd" value="http://omniverse-content-production.s3-us-west-2.amazonaws.com/Samples/Astronaut/Astronaut.usd">
51                            </div>
52                        </p>
53                        <div>
54                            <button type="button" class="btn btn-primary mt-2" id="capture-viewport">Capture Viewport</button>
55                            <div class="alert alert-danger d-flex align-items-center mt-3 mb-0 d-none" id="error-message" role="alert"></div>
56                        </div>
57                    </div>
58                    <img id="viewport-capture-image" src="" class="card-img-bottom d-none" alt="Viewport Capture">
59                </div>
60            </div>
61        </div>
62    </main>
63
64    <!-- Footer of the page: -->
65    <footer class="text-body-secondary py-4">
66        <div class="container">
67            <p class="float-end mb-1">
68                <a href="#">Back to top</a>
69            </p>
70            <p class="mb-0">New to Omniverse? <a href="https://www.nvidia.com/omniverse">Visit the homepage</a> or read our <a href="https://docs.omniverse.nvidia.com">documentation guides</a>.</p>
71        </div>
72    </footer>
73
74    <!-- Import scripts used for the application: -->
75    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm" crossorigin="anonymous"></script>
76    <script src="./app.js" type="module"></script>
77</body>
78</html>

CSS

As you may have seen from looking at the HTML above, we are basing our application’s style on the versatile and well-established Bootstrap front-end framework for the purposes of this tutorial. This should make it simple to expand this example in the future, should you wish to use it as the foundation for your future endeavours, or to customize to match other web resources you might already be hosting as part of the system you are designing.

As we are hoping to provide a more personalised experience branded after the Omniverse theme, we will be making a few stylistic differences from the default Bootstrap theme available with the framework. Let’s customize a few CSS rules in our application’s style definition file:

Click to expand the CSS code...
exts/omni.services.example.viewport_capture.ui/web/style.css
  1/**
  2 * Setup for old browsers:
  3 */
  4@supports not (font-variation-settings: normal) {
  5    @font-face {
  6        font-family: 'NVIDIA Sans';
  7        src: url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/NVIDIASans_W_Lt.woff') format('woff'),
  8             url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/NVIDIASans_W_Lt.woff2') format('woff2');
  9        font-weight: 300;
 10        font-style: normal;
 11    }
 12    @font-face {
 13        font-family: 'NVIDIA Sans';
 14        src: url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/NVIDIASans_W_Rg.woff') format('woff'),
 15             url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/NVIDIASans_W_Rg.woff2') format('woff2');
 16        font-weight: 400;
 17        font-style: normal;
 18    }
 19    @font-face {
 20        font-family: 'NVIDIA Sans';
 21        src: url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/NVIDIASans_W_Md.woff') format('woff'),
 22             url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/NVIDIASans_W_Md.woff2') format('woff2');
 23        font-weight: 500;
 24        font-style: normal;
 25    }
 26    @font-face {
 27        font-family: 'NVIDIA Sans';
 28        src: url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/NVIDIASans_W_Bd.woff') format('woff'),
 29             url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/NVIDIASans_W_Bd.woff2') format('woff2');
 30        font-weight: 700;
 31        font-style: normal;
 32    }
 33}
 34
 35/**
 36 * Setup for modern browsers, all weights:
 37 */
 38@supports (font-variation-settings: normal) {
 39    @font-face {
 40        font-family: 'NVIDIA Sans';
 41        src: url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/var/NVIDIASansVF_W_Wght.woff2') format('woff2 supports variations'),
 42             url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/var/NVIDIASansVF_W_Wght.woff2') format('woff2-variations');
 43        font-weight: 100 1000;
 44        font-stretch: 25% 151%;
 45        font-style: normal;
 46    }
 47    @font-face{
 48        font-family: 'NVIDIA Sans';
 49        src: url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/var/NVIDIASansVF_Wght_W_Italic.woff2') format('woff2 supports variations'),
 50             url('https://images.nvidia.com/etc/designs/nvidiaGDC/clientlibs_base/fonts/nvidia-sans/GLOBAL/var/NVIDIASansVF_Wght_W_Italic.woff2') format('woff2-variations');
 51        font-weight: 100 1000;
 52        font-stretch: 25% 151%;
 53        font-style: italic;
 54    }
 55}
 56
 57
 58body {
 59    --bs-dark-rgb: 0, 0, 0;
 60    --bs-tertiary-bg-rgb: 41, 41, 41;
 61    font-family: "NVIDIA Sans";
 62
 63    min-height: 100vh;
 64    display: flex;
 65    flex-direction: column;
 66    --bs-body-bg: rgb(52, 52, 52);
 67}
 68
 69.card {
 70    --bs-border-radius: 0;
 71    --bs-card-bg: rgb(52, 52, 52);
 72}
 73
 74.btn:hover {
 75    --bs-btn-color: rgb(145, 199, 51);
 76}
 77.btn-primary,
 78.btn-secondary {
 79    --bs-border-radius: 0;
 80    --bs-btn-bg: rgb(118, 185, 0);
 81    --bs-btn-border-width: 0;
 82    --bs-btn-color: rgb(0, 0, 0);
 83    --bs-btn-font-weight: 700;
 84    --bs-btn-hover-bg: rgb(145, 199, 51);
 85    --bs-btn-hover-color: rgb(0, 0, 0);
 86    --bs-btn-padding-y: var(--bs-btn-padding-x);
 87}
 88.btn:disabled,
 89.btn-primary:disabled {
 90    --bs-btn-disabled-color: var(--bs-btn-color);
 91    --bs-btn-disabled-bg: var(--bs-btn-bg);
 92}
 93.btn-secondary {
 94    --bs-btn-bg: transparent;
 95    --bs-btn-border-color: rgb(118, 185, 0);
 96    --bs-btn-border-width: 2px;
 97    --bs-btn-color: rgb(255, 255, 255);
 98}
 99.btn-secondary:hover {
100    --bs-btn-hover-border-color: rgb(145, 199, 51);
101}
102
103.form-control {
104    --bs-tertiary-bg-rgb: rgb(41, 41, 41);
105    background-color: var(--bs-tertiary-bg-rgb);
106}
107.form-control:focus {
108    background-color: var(--bs-tertiary-bg-rgb);
109    border-color: rgb(145, 199, 51);
110    box-shadow: 0 0 0.25rem rgba(118, 185, 0, 0.25) !important;
111}
112
113main,
114footer {
115    background-color: rgb(32, 32, 32);
116}
117main {
118    min-height: 100vh;
119}
120footer a {
121    color: var(--bs-secondary-color);
122}

JavaScript

With our style customizations now completed, let’s implement the application logic of our microsite. The JavaScript file containing the event handlers for our User interactions, network communication with our API Service and error handlers will bundle all our desired behaviors in a minimalistic way.

This will allow us to define features such as:

  • Sending requests over the network to the Service API we defined earlier, in order to request that a screenshot of the viewport be captured.

  • Displaying the image captured from the Service, once it is available.

  • Displaying notifications to the User in case of errors, for example in cases where the OpenUSD Stage at the given location could not be loaded, or if network communication with the Service API could not be established.

  • etc.

Click to expand the JavaScript code...
exts/omni.services.example.viewport_capture.ui/web/app.js
  1/**
  2 * Unique identifier of the DOM Element responsible for displaying error messages to the User.
  3 */
  4const ERROR_MESSAGE_ELEMENT_ID = 'error-message';
  5/**
  6 * Unique identifier of the DOM ELement responsible for displaying the image of the viewport captured from the
  7 * application.
  8 */
  9const VIEWPORT_CAPTURE_IMAGE_ELEMENT_ID = 'viewport-capture-image';
 10/**
 11 * Unique identifier of the DOM Element responsible for providing the URL of the OpenUSD Stage to load in the Omniverse
 12 * application.
 13 */
 14const STAGE_LOCATION_INPUT_ELEMENT_ID = 'stage-location';
 15/**
 16 * Unique identifier of the DOM Element responsible for triggering the capture action (i.e. the button on which the User
 17 * will click to initiate the capture process).
 18 */
 19const CAPTURE_VIEWPORT_BUTTON_ELEMENT_ID = 'capture-viewport';
 20
 21
 22/**
 23 * Display the given error message to the User.
 24 *
 25 * @param {string} errorMessage Error message to display to the User.
 26 */
 27function showError(errorMessage) {
 28    const alertMessage = document.getElementById(ERROR_MESSAGE_ELEMENT_ID);
 29    if (alertMessage !== null) {
 30        alertMessage.textContent = errorMessage;
 31        alertMessage.classList.remove('d-none');
 32    }
 33}
 34
 35/**
 36 * Hide any error message currently displayed to the User.
 37 */
 38function hideError() {
 39    const alertMessage = document.getElementById(ERROR_MESSAGE_ELEMENT_ID);
 40    if (alertMessage !== null) {
 41        alertMessage.classList.add('d-none');
 42    }
 43}
 44
 45/**
 46 * Handler for the action of capturing the remote application viewport.
 47 *
 48 * @param {PointerEvent} event Event raised when clicking the button to capture the remote viewport.
 49 */
 50async function onCaptureViewport(event) {
 51    // Hide error message potentially left over during the previous capture:
 52    hideError();
 53
 54    // Update the "Capture" button to provide feedback to the User about the capture process being initiated:
 55    const viewportCaptureButton = event.target;
 56    let originalViewportCatpureButtonLabel = null;
 57    if (viewportCaptureButton !== null) {
 58        viewportCaptureButton.disabled = true;
 59        originalViewportCatpureButtonLabel = viewportCaptureButton.textContent;
 60        viewportCaptureButton.textContent = 'Capturing Viewport...';
 61    }
 62
 63    // Disable the "Capture" button to prevent the User from submitting a second capture request before the response
 64    // from the first one has been received, then parse the incoming data to ensure that a successful result has been
 65    // received:
 66    const viewportCaptureImage = document.getElementById(VIEWPORT_CAPTURE_IMAGE_ELEMENT_ID);
 67    const usdStageLocationInput = document.getElementById(STAGE_LOCATION_INPUT_ELEMENT_ID);
 68    if (viewportCaptureImage !== null && usdStageLocationInput !== null) {
 69        viewportCaptureImage.classList.add('d-none');
 70        usdStageLocationInput.setAttribute('readonly', true);
 71
 72        try {
 73            // Send a request to the Omniverse Service, awaiting a response with the result of the operation:
 74            const response = await fetch('/viewport-capture/capture', {
 75                method: 'POST',
 76                headers: {
 77                    'content-type': 'application/json',
 78                },
 79                body: JSON.stringify({
 80                    'usd_stage_path': usdStageLocationInput.value,
 81                }),
 82            });
 83
 84            // Parse the incoming response, displaying the captured image in the case of a successful operation or a
 85            // notification to the User with the content of the incoming error message in the case of an unsuccessful
 86            // operation:
 87            const data = await response.json();
 88            if ('success' in data) {
 89                if (data['success'] === true && 'captured_image_path' in data) {
 90                    const screenshot_path = data['captured_image_path'];
 91                    viewportCaptureImage.src = screenshot_path;
 92                    viewportCaptureImage.classList.remove('d-none');
 93                } else if ('error_message' in data) {
 94                    // Display the error message issued by the Service, if any:
 95                    showError(data['error_message']);
 96                }
 97            }
 98        } catch (ex) {
 99            // In case of an error such as a network exception, display a notification to the User with additional
100            // details in order to provide some feedback about the process:
101            showError(ex.message);
102        }
103    }
104
105    // Enable the form inputs after receiving a response from the Service, so the User can submit an new request if
106    // desired:
107    if (viewportCaptureButton !== null) {
108        viewportCaptureButton.textContent = originalViewportCatpureButtonLabel;
109        viewportCaptureButton.disabled = false;
110    }
111    usdStageLocationInput.removeAttribute('readonly');
112}
113
114/**
115 * Register event listeners on the DOM Elements of the document in order to respond to User actions.
116 */
117function registerEventListeners() {
118    // Register "click" listeners on the button to initiate the capture process:
119    const captureViewportButton = document.getElementById(CAPTURE_VIEWPORT_BUTTON_ELEMENT_ID);
120    if (captureViewportButton !== null) {
121        captureViewportButton.addEventListener('click', onCaptureViewport);
122    }
123
124    // Register text input listeners on the field used to provide the URL of the OpenUSD Stage to open:
125    const usdStageLocationInput = document.getElementById(STAGE_LOCATION_INPUT_ELEMENT_ID);
126    if (usdStageLocationInput !== null) {
127        usdStageLocationInput.addEventListener('input', e => {
128            captureViewportButton.disabled = e.target.value.length === 0;
129        });
130    }
131}
132
133
134/**
135 * Main application entrypoint.
136 */
137(function main() {
138    // Register the event listeners on the DOM Elements of the document in order to respond to User actions:
139    registerEventListeners();
140})();

Testing the microsite

With the microsite code now completed, we can experience it by launching an instance of our Omniverse application with our extension enabled, and navigating to http://localhost:8011/viewport-capture to confirm that the front-end for our viewport capture Service is accessible over the network.

Open a terminal from the location where Omniverse Code was installed from the Omniverse Launcher, and use it to map and enable the prototype of our new frontend UI extension:

On Windows:

1kit.exe ^
2    ./apps/omni.code.kit ^
3    --ext-folder <path of the "/exts" folder of the forked the Kit template repository> ^
4    --enable omni.services.example.viewport_capture.ui

On Linux:

1./kit \
2    ./apps/omni.code.kit \
3    --ext-folder <path of the "/exts" folder of the forked the Kit template repository> \
4    --enable omni.services.example.viewport_capture.ui \
5    --allow-root

Next steps

With the experience gained from this brief tutorial where we showcased the benefits of maintaining a clear separation of concerns between components, we hope this can inspire you to continue your journey creating engaging features for your Users.

Should you be looking for ideas to practise this newly-acquired knowledge, some potential projects that may be of interest include:

  • Using the Omniverse Farm Queue APIs to build custom submission forms for tasks that may require User input for multiple fields. Using familiar web forms may be a convenient way for Users to configure the Farm jobs they wish to execute, and allow them to schedule them from devices such as their phones or tablets, without the need to build dedicated native applications for Android and/or iOS.

  • Interfacing with other Omniverse APIs such as the RunUSD validation service to ensure that OpenUSD assets conform to specific rules dictating their structure.

  • Building interactive AI agents to engage with Users in new and creative ways, either to automate tasks within Omniverse applications or within workflows in general.

  • etc.

 
« Previous section: Containerizing the service