VR Sync API: Creating a VR Sync Client

This quickstart can be used as a reference guide to building your own VR Sync Client. In this guide we will run through the steps for creating an application that listens to the VR Sync server to run a motion simulator pod alongside the VR video playback.


Setting up a connection

Once our client application has initialized our motion pod software and is ready for playback, the connection to the VR Sync Server can be established. In our case, we have a VR Sync Box. So we can install Socket.io for our development environment and establish a connection to 172.28.1.9, using port 7327.

this.socket = io("http://172.28.1.9:7327", {
    transports: ["websocket"]
});
this.socket.on("connect", () => { this.onConnect(); });
this.socket.on("disconnect", (reason) => { this.onDisconnect(reason); });
this.socket.on("message", (message) => { this.messageReceived(message); });
this.socket.open();


Keeping the connection alive

In order to keep a connection with the server, our client application needs to send some status update messages. These messages are called Ping messages and can contain information about the device’s current status for viewing by the webremote. 

onConnect() {
   this.ping();
   this.pingInterval = setInterval(this.ping.bind(this), 3000);
}
ping() {
    const ping = {
        type: "Ping",
        sender: "Client",
        protocolVersion: 2,
        // The Unix UTC timestamp of the sender's clock when the message was sent, in milliseconds
        sentUnixTimestampMs: 1721807772071,
        // Increment this ID for each ping message. This can be used for calculating the latency.
        id: 1,
        deviceStatus: {
            // This is the preferred name for our device. If the Server does not have a name for the device already, it will use this name.
            deviceName: "Device name",
            // The platform or client type.
            clientPlatform: "CustomMotionPlatform",
            // Charging or Discharging.
            batteryStatus: "Charging",
            // an Integer from 0 to 100.
            batteryPercentage: 100,
            // A unique identifier for the device that stays the same in-between sessions. Such as a hardware id or MAC-address. Should not contain spaces!
            deviceUID: "000001",
            // The version of your application. We recommend to keep this in-line with VR Sync version numbers
            appVersion: "3.5.0"
        }
    };
    this.socket.emit("message", ping);
    this.emit(Events.PingSent, ping);
}

Not sending the ping within at least a 5 second interval can result in a disconnect.

Note the ping Id, this id will be sent back from the server. This allows you to count the amount of ms between a message roundtrip (latency). Averaging latencies and filtering outliers, together with the the sentUnixTimestampMs, allows you to implement clock offset calculation using Cristian’s algorithm. That way you can synchronize your content with far less influence from single-message latency spikes.


Receive playback commands

At any time during the connection, the client can receive a new command message from the server. The server will send the latest command status upon connection. Commands will look as follows:

{
    // This message came from the server
    "sender": "Server",
    // This is a media command 
    "type": "Command",
    "protocolVersion": 2,
    // The Unix UTC timestamp of the sender's clock when it received the command from an Admin client, in milliseconds
    "serverReceived": 1721807772071,
    // The Unix UTC timestamp of the sender's clock when it received the pause command from an Admin client, in milliseconds. Use (pauseTime - serverReceived) to determine the time at which to pause the video. This is only included if "paused" is true.
    "pauseTime": 1721807772071,
    // The playlist should not loop
    "loop": false,
    // The playback is not paused
    "paused": false,
    // The amount of milliseconds since the playback started
    "currentTime": 0,
    // The Unix UTC timestamp of the sender's clock when it received the command from an Admin client, in milliseconds
    "serverReceived": 1721807772071,
    "playlist": [
        {
            // LocalVideo, CloudVideo, LocalImage, CloudImage
            "type": "LocalVideo",
            // The ID of the media file
            "identifier": "2",
            // The duration of the media file
            "durationinMs": 240000,
            // The amount of milliseconds to wait before starting playback (to compensate buffering/loading)
            "playDelayMs": 4000
        }
    ]
}

The playlist array usually contains a list of items that should be played in sequence. By adding half of the average latency over the last few pings, you can calculate the current progress in the current playlist. Make sure to account for the duration of the media, the play delay and the loop value. If the video playback has concluded, the command can still contain this playlist, but the currentTime will be out of bounds. The most simple play command will have only one playlist item. A stop command can be seen as a command with an empty playlist.

An example implementation:

messageReceived(message) {
    if (message.type === "Ping") {
        // Calculate the average latency by matching this ping message with the timestamp it was sent to the server
        this.latency.push(calculateLatencyMs(message.time.id));
    } else if (message.type === "Command") {
        // Calculate the elapsed time and play the motion pod file
        const currentMedia = this.findPlayingMedia(message);
        if (currentMedia.playingMedia != null) {
            this.motionPod.play({
                media: currentMedia.playingMedia,
                timestamp: currentMedia.mediaProgress
            });
        } else if (!this.motionPod.hasStopped) {
            this.motionPod.stop();
        }
    }
}
findPlayingMedia(command) {
    const playlist = command.playlist;
    const currentTime = command.currentTime;
    const loop = command.loop;
    // Calculate total playlist duration
    let totalPlaylistDuration = 0;
    playlist.forEach((item) => {
        totalPlaylistDuration += item.durationinMs + item.playDelayMs;
    });
    // Adjust currentTime for looping
    let adjustedTime = currentTime;
    if (loop) {
        adjustedTime %= totalPlaylistDuration;
    }
    // Find the playing media
    let cumulativeTime = 0;
    let playingMedia = null;
    let mediaProgress = 0;
    for (const item of playlist) {
        const mediaStart = cumulativeTime + item.playDelayMs;
        const mediaEnd = mediaStart + item.durationinMs;
        if (adjustedTime >= mediaStart && adjustedTime < mediaEnd) {
            playingMedia = item;
            mediaProgress = adjustedTime - mediaStart; // Progress in the current media
            break;
        }
        // Update the cumulative time
        cumulativeTime = mediaEnd;
    }
    return { playingMedia, mediaProgress };
}

You now know how to connect and receive basic VR Sync commands. For further reading, we recommend you check out our other quickstart guides for sending VR Sync commands, or read our API guide.


Related

VR Sync API: Introduction Read

VR Sync API: Creating a VR Sync Remote Read

VR Sync API: Documentation Read