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#

  1. CloudXRKit.Session: Manages available channels and provides access to them

  2. MessageChannel: Represents an individual communication channel

  3. 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:

  1. Discovering available channels

  2. Opening a channel

  3. Sending messages

  4. Receiving messages

  5. Monitoring channel status

  6. 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:

  1. Server creates channel: xrCreateOpaqueDataChannelNV() with a UUID.

  2. Client discovers channel: It appears in availableMessageChannels.

  3. Client opens channel: getMessageChannel() initiates the connection.

  4. Both wait for ready state: Server polls for CONNECTED, client for ready.

  5. Exchange messages: The server sends/receives data with xrSendOpaqueDataChannelNV and xrReceiveOpaqueDataChannelNV. The client sends with sendServerMessage and receives by reading from receivedMessageStream.

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)