Custom Message Handling in the Omniverse on DGX Cloud Portal Sample to Support Authentication Flows#
For the public version of the Portal Sample file, see useStream.ts on GitHub.
Adapted Version with NavVis IVION Authentication#
Adapted useStream.ts with NavVis IVION support#
import { hideNotification, notifications } from "@mantine/notifications";
import {
AppStreamer,
DirectConfig,
eAction,
eStatus,
LogFormat,
LogLevel,
StreamEvent,
StreamType,
} from "@nvidia/omniverse-webrtc-streaming-library";
import { useCallback, useEffect, useRef, useState } from "react";
import { Config } from "../providers/ConfigProvider";
import { StreamingApp } from "../state/Apps";
import { useConfig } from "./useConfig";
import useError from "./useError";
import useStreamStart, {
showStreamWarning,
streamStartNotification,
} from "./useStreamStart";
export interface UseStreamOptions {
app: StreamingApp;
sessionId: string;
videoElementId?: string;
audioElementId?: string;
}
export interface UseStreamResult {
loading: boolean;
error: Error | string;
terminate: () => Promise<void>;
}
export default function useStream({
app,
sessionId,
videoElementId = "stream-video",
audioElementId = "stream-audio",
}: UseStreamOptions): UseStreamResult {
const config = useConfig();
const [loading, setLoading] = useState(false);
const [error, setError] = useError();
const initialized = useRef(false);
const { mutateAsync: startNewSession } = useStreamStart(app.id);
const startNewSessionRef = useRef(startNewSession);
startNewSessionRef.current = startNewSession;
useEffect(() => {
if (!sessionId) {
return;
}
if (initialized.current) {
return;
}
initialized.current = true;
setLoading(true);
setError("");
function onUpdate(message: StreamEvent) {
console.log("onUpdate", message);
}
function onStart(message: StreamEvent) {
console.log("onStart", message);
if (message.action === eAction.start) {
if (message.status === eStatus.success) {
const video = document.getElementById(
videoElementId,
) as HTMLVideoElement;
video.play().catch((error) => {
setError(error as Error);
});
setLoading(false);
hideNotification(streamStartNotification);
} else if (message.status === eStatus.error) {
setError(message.info || "Unknown error.");
setLoading(false);
} else if (message.status === eStatus.warning) {
showStreamWarning();
}
}
}
function onStop(message: StreamEvent) {
console.log("onStop", message);
}
function onTerminate(message: StreamEvent) {
console.log("onTerminate", message);
}
function onStreamStats(message: StreamEvent) {
console.log("onStreamStats", message);
}
// Prefix used for local storage items specific to the omni.pointcloud.potree2 extension
const POTREE2_STORAGE_PREFIX: string = "omni.pointcloud.potree2:";
// Removes expired local storage items that were received from the omni.pointcloud.potree2
// extension. When "clear_all" is true, all of those items will be removed, independent of their
// expiration status.
function expirePotree2Storage(clear_all: boolean = false): void {
// Retrieve all keys that have the prefix
var keys: string[] = [];
for (var i=0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key != null && key.startsWith(POTREE2_STORAGE_PREFIX)) {
keys.push(key);
}
}
for (const key of keys) {
if (clear_all) {
console.info("Removing storage entry '" + key + "'");
localStorage.removeItem(key);
}
else {
const data_json = localStorage.getItem(key)
if (data_json != null) {
const data = JSON.parse(data_json);
if ("expiration_time" in data) {
const now = Date.now() / 1000; // seconds since epoch
if (now >= data.expiration_time) {
console.info("Removing expired storage entry '" + key + "'");
localStorage.removeItem(key);
}
}
}
}
}
}
// Handles messages sent by the omni.pointcloud.potree2 extension.
// Returns true if the message was processed, false otherwise.
function handlePotree2Event (message: unknown): boolean {
if (!message || !("event_type" in message && "payload" in message)) {
return false;
}
// open_url: Open the requested URL in a new browser tab.
// Note: This may trigger the browser's pop-up blocker under certain conditions.
if (message.event_type === "omni.pointcloud.potree2@open_url") {
if ("url" in message.payload) {
const url = message.payload.url;
console.info(message.event_type + ": Opening URL in new tab:", url);
window.open(url, '_blank', 'noopener,noreferrer');
}
return true;
}
// ping: Answer with "ping_result", letting Kit know that a client is connected.
if (message.event_type === "omni.pointcloud.potree2@ping") {
const answer = {
event_type: message.event_type + "_result",
payload: true
};
console.info(message.event_type + ": Sending answer.");
AppStreamer.sendMessage(JSON.stringify(answer));
return true;
}
// store_data: Store the received value in the browser's local storage.
// Expiration will be handled by expirePotree2Storage().
//
// Note: Instead of using local storage here, the data could also be passed on to another web
// service for storage.
if (message.event_type === "omni.pointcloud.potree2@store_data") {
var key = message.payload.key;
if (key != "") {
const data = {
value: message.payload.value,
expiration_time: message.payload.expiration_time
}
localStorage.setItem(POTREE2_STORAGE_PREFIX + key, JSON.stringify(data));
console.info(message.event_type + ": Stored data for key '" + POTREE2_STORAGE_PREFIX + key + "'");
}
return true;
}
// get_data: Return data from local storage that was previously sent with "store_data".
// Answer with "get_data_result" and the retrieved value.
if (message.event_type === "omni.pointcloud.potree2@get_data") {
// Remove expired items
expirePotree2Storage();
var key = message.payload.key;
if (key != "") {
const data_json = localStorage.getItem(POTREE2_STORAGE_PREFIX + key)
var value = null;
if (data_json != null) {
const data = JSON.parse(data_json)
if ("value" in data) {
// Valid item was found
value = data.value;
// Check its expiration again
if ("expiration_time" in data) {
const now = Date.now() / 1000; // seconds since epoch
if (now >= data.expiration_time) {
console.info(message.event_type + ": Requested storage entry is expired")
localStorage.removeItem(POTREE2_STORAGE_PREFIX + key);
value = null
}
}
}
}
// Prepare answer; value will be null if no stored item was found
const answer = {
event_type: message.event_type + "_result",
payload: {key: key, value: value}
};
console.info(message.event_type + ": Sending " + (value == null ? "empty " : "")
+ "answer for key '" + POTREE2_STORAGE_PREFIX + key + "'");
AppStreamer.sendMessage(JSON.stringify(answer));
}
return true;
}
return false;
}
function onCustomEvent(message: unknown) {
console.log("onCustomEvent", message);
if (handlePotree2Event(message)) {
return;
}
}
const params = createStreamConfig(app, sessionId, config);
async function connect() {
try {
const sessionExists = await checkSession(sessionId, config);
if (!sessionExists) {
notifications.show({
id: streamStartNotification,
message:
"This session is no longer available, starting a new streaming session...",
loading: true,
autoClose: 30000,
});
try {
return await startNewSessionRef.current();
} catch (error) {
setError(error as Error);
setLoading(false);
}
}
await AppStreamer.connect({
streamSource: StreamType.NVCF,
logLevel: LogLevel.DEBUG,
logFormat: LogFormat.TEXT,
streamConfig: {
videoElementId,
audioElementId,
maxReconnects: 3,
nativeTouchEvents: true,
...params,
onUpdate,
onStart,
onStop,
onTerminate,
onStreamStats,
onCustomEvent,
},
});
} catch (error) {
setError(
"info" in (error as StreamEvent)
? (error as StreamEvent).info
: (error as Error),
);
setLoading(false);
}
}
async function start() {
console.log("Start streaming...");
await connect();
}
void start();
return () => {
if (import.meta.env.PROD) {
void AppStreamer.terminate();
}
};
}, [app, sessionId, videoElementId, audioElementId, config, setError]);
const terminate = useCallback(async () => {
try {
await AppStreamer.terminate(true);
} catch (error) {
setError(
"info" in (error as StreamEvent)
? (error as StreamEvent).info
: (error as Error),
);
console.error("Error terminating stream:", error);
}
}, [setError]);
return {
loading,
error,
terminate,
};
}
async function checkSession(
sessionId: string,
config: Config,
): Promise<boolean> {
const url = createStreamURL(sessionId, config);
url.pathname += "/sign_in";
try {
const response = await fetch(url, { method: "HEAD" });
return response.ok;
} catch (error) {
console.error(`Failed to check the current streaming session:`, error);
return false;
}
}
/**
* Creates URL parameters for streaming the application from NVCF.
* Returns URLSearchParams instance with values that must be passed to streamConfig object in
* the `urlLocation.search` field.
*
* @param app
* @param sessionId
* @param config
* @returns {URLSearchParams}
*/
function createStreamConfig(
app: StreamingApp,
sessionId: string,
config: Config,
): Partial<DirectConfig> {
const params: DirectConfig = {
width: 1920,
height: 1080,
fps: 60,
mic: false,
cursor: "free",
autoLaunch: true,
// Specifies that the default streaming endpoint must not be used.
// Enables signaling parameters for the component.
server: "",
};
// If specified, enables the private endpoint created in Azure
if (app.mediaServer) {
params.mediaServer = app.mediaServer;
if (app.mediaPort) {
params.mediaPort = app.mediaPort;
}
}
const signalingURL = createStreamURL(sessionId, config);
params.signalingServer = signalingURL.hostname;
params.signalingPort = signalingURL.port
? Number(signalingURL.port)
: signalingURL.protocol === "https:"
? 443
: 80;
params.signalingPath = signalingURL.pathname;
params.signalingQuery = signalingURL.searchParams;
return params;
}
/**
* Constructs a URL object for streaming the specified NVCF function.
*
* @param sessionId
* @param config
* @returns {URL}
*/
function createStreamURL(sessionId: string, config: Config): URL {
let backend = config.endpoints.backend;
if (!backend.endsWith("/")) {
backend += "/";
}
return new URL(`./sessions/${sessionId}`, backend);
}
Usage Notes#
Paste this adapted hook into your Portal Sample codebase (for example
web/src/hooks/useStream.ts) to add NavVis IVION-related handling andomni.pointcloud.potree2extension message processing.Keep a copy of the public upstream file for reference: useStream.ts on GitHub.