Message Channel API#
Overview#
The Message Channel API (also called the CloudXR Opaque Data Channel) provides a bidirectional communication mechanism between NVIDIA CloudXR™ client and server applications. This allows you to send custom data alongside the video/audio streaming, enabling rich interactions between client and server.
On the server side, this requires the app to implement the XR_NV_opaque_data_channel extension (see XR Opaque Data Channel).
Key Concepts#
Message Channels#
Channel: A bidirectional communication pipe identified by a unique UUID
Channel Info: Carries channel metadata (ID and UUID)
Channel Status: Tracks the state of a channel (Not Initialized, Ready, or Closed)
Components#
CloudXRKit.Session: Manages available channels and provides access to them
MessageChannel: Represents an individual communication channel
MessageChannelManager: Internal component that handles the channel lifecycle
API Usage#
The client performs six operations during the lifecycle of a channel. Roughly in order of use, they are:
Discovering available channels
Opening a channel
Sending messages
Receiving messages
Monitoring channel status
Closing the channel
Discovering Available Channels#
Channels are announced by the server. To discover them, the client can monitor the session’s availableMessageChannels property:
// Access available channels from your CloudXRKit.Session instance
let availableChannels = cxrSession.availableMessageChannels
// Each ChannelInfo contains:
// - channelId: Internal numeric identifier
// - uuid: Unique identifier as Data (can be converted to string)
Opening a Channel#
To start using a channel, retrieve it from the session:
// Select a channel from available channels
let channelInfo = cxrSession.availableMessageChannels.first
// Get the MessageChannel object
if let channel = cxrSession.getMessageChannel(channelInfo) {
// Channel is now ready to use
print("Channel status: \(channel.status)")
}
Sending Messages#
To send data to the server using the channel:
// Convert your data to Data format
let message = "Hello Server"
if let data = message.data(using: .utf8) {
let success = channel.sendServerMessage(data)
if success {
print("Message sent successfully")
}
}
// You can send any binary data
let binaryData = Data([0x01, 0x02, 0x03, 0x04])
channel.sendServerMessage(binaryData)
Receiving Messages#
Messages from the server are delivered through an AsyncStream:
// Set up a task to receive messages
Task {
for await messageData in channel.receivedMessageStream {
// Process received data
if let message = String(data: messageData, encoding: .utf8) {
print("Received: \(message)")
} else {
// Handle binary data
print("Received \(messageData.count) bytes")
}
}
// Stream ended - channel is closed
print("Channel closed")
}
Monitoring Channel Status#
You must monitor the channel status to ensure proper operation:
switch channel.status {
case .notInitialized:
print("Channel not yet initialized")
case .ready:
print("Channel ready for communication")
case .closed:
print("Channel has been closed")
}
Closing the Channel#
You must explicitly close a channel when the client is done using it:
channel.disconnect()
A Complete Example#
Here’s a complete example showing typical usage:
import CloudXRKit
import SwiftUI
struct MessageChannelExample: View {
let cxrSession: CloudXRKit.Session
@State private var currentChannel: MessageChannel?
@State private var receivedMessages: [String] = []
@State private var readerTask: Task<Void, Never>?
var body: some View {
VStack {
// Channel selection
if let firstChannel = cxrSession.availableMessageChannels.first {
Button("Open Channel") {
openChannel(firstChannel)
}
}
// Send message
if currentChannel?.status == .ready {
Button("Send Hello") {
sendMessage("Hello from client!")
}
}
// Display received messages
List(receivedMessages, id: \\.self) { message in
Text(message)
}
}
.onDisappear {
// Clean up
readerTask?.cancel()
currentChannel?.disconnect()
}
}
func openChannel(_ channelInfo: ChannelInfo) {
guard let channel = cxrSession.getMessageChannel(channelInfo) else {
print("Failed to open channel")
return
}
currentChannel = channel
// Start receiving messages
readerTask = Task {
for await data in channel.receivedMessageStream {
if let message = String(data: data, encoding: .utf8) {
await MainActor.run {
receivedMessages.append(message)
}
}
}
}
}
func sendMessage(_ text: String) {
guard let data = text.data(using: .utf8),
let channel = currentChannel else { return }
_ = channel.sendServerMessage(data)
}
}
Best Practices#
Error Handling:
Always check whether
getMessageChannel()returns a valid channel.Verify that
sendServerMessage()returns a normal value for send confirmation.Handle channel closure gracefully in your receive loop.
Data Encoding:
Use consistent encoding between client and server (e.g., UTF-8 for text).
Consider using structured formats (JSON, Protocol Buffers) for complex data.
Handle binary data appropriately.
Resource Management:
Cancel reader tasks when they are no longer needed.
Disconnect channels explicitly when the client is done using them.
Monitor channel status changes.
Performance Considerations:
Message channels share bandwidth with video/audio streaming.
Avoid sending large amounts of data frequently.
Consider batching small messages.
Advanced Usage#
Custom Data Formats#
For structured data, consider using JSON:
struct GameState: Codable {
let playerPosition: SIMD3<Float>
let score: Int
}
// Sending
let gameState = GameState(playerPosition: [1, 2, 3], score: 100)
if let jsonData = try? JSONEncoder().encode(gameState) {
channel.sendServerMessage(jsonData)
}
// Receiving
for await data in channel.receivedMessageStream {
if let gameState = try? JSONDecoder().decode(GameState.self, from: data) {
print("Player at: \(gameState.playerPosition), Score: \(gameState.score)")
}
}
Multiple Channels#
You can manage multiple channels simultaneously:
var activeChannels: [ChannelInfo: MessageChannel] = [:]
// Open multiple channels
for channelInfo in cxrSession.availableMessageChannels {
if let channel = cxrSession.getMessageChannel(channelInfo) {
activeChannels[channelInfo] = channel
// Set up individual receivers for each channel
}
}
Troubleshooting#
Channel Not Available:
Check whether the server has announced the channel.
Check whether the session is in the connected state.
Verify that the client and server use the same channel UUID.
Messages Not Received:
Confirm that the channel status is
.ready.Check that the receive task is running.
Verify that the sender and receiver use the same data encoding.
Channel Closes Unexpectedly:
Monitor server-side logs for errors.
Check network connectivity.
Ensure proper error handling in message processing.
How Server and Client Connect:
Server creates channel:
xrCreateOpaqueDataChannelNV()with a UUID.Client discovers channel: It appears in
availableMessageChannels.Client opens channel:
getMessageChannel()initiates the connection.Both wait for ready state: Server polls for CONNECTED, client for ready.
Exchange messages: The server sends/receives data with
xrSendOpaqueDataChannelNVandxrReceiveOpaqueDataChannelNV. The client sends withsendServerMessageand receives by reading fromreceivedMessageStream.
Key Differences:
Shutdown vs destroy:
xrShutdownOpaqueDataChannelNV: Gracefully closes connection (client sees “closed”).xrDestroyOpaqueDataChannelNV: Frees resources (must be called after shutdown).
Why server waits: Although the server creates the channel, it must wait for client connection due to the “server announces, client detects, client opens” protocol.
Example Pairing#
Server (C++):
// Create with known UUID
xrCreateOpaqueDataChannelNV(instance, &createInfo, &channel);
WaitForConnection(channel); // Poll until CONNECTED
xrSendOpaqueDataChannelNV(channel, len, data);
Client (Swift):
// Find matching UUID
let channel = cxrSession.getMessageChannel(channelInfo)!
channel.sendServerMessage(responseData)