Content synchronization, Sync Server, and video walls
devSpaceDevSpaceSync APIdevSpaceSyncAPI👑 DevSpace add-onIntroduction​
Content synchronization lets multiple devices play related content at the same time. Common use cases include video walls, menu boards, synchronized ambience screens, and any applet that needs several displays to switch content together.
Synchronization in signageOS is exposed through the Sync API. Your applet joins a sync group, shares the next value to play, waits until the group reaches the same point, and then starts playback through the device-native player.
Architecture overview​
There are two synchronization architectures:
| Architecture | Best for | How devices communicate |
|---|---|---|
| P2P sync | Lowest-latency local deployments where devices are on the same LAN | Devices communicate directly over the local network. |
| Sync Server | Deployments where direct device-to-device communication is unavailable or where a central WebSocket server is preferred | Devices connect to a synchronization service over WebSocket. |
The Sync API keeps your applet code mostly independent from the transport. You choose the engine and server URL during sos.sync.connect(), then use the same group and wait/broadcast flow.
The signageOS-hosted Applet Synchronizer service was deprecated and shut down on December 31, 2024. See the Hosted Applet-Synchronizer Service deprecation notice.
For new deployments, use P2P sync when devices can communicate on the same local network, or deploy your own on-premise Synchronizer when you need a server-based architecture.
P2P sync​
P2P sync is recommended when devices are installed on the same local network and can communicate with each other directly. It avoids a central server for time-sensitive synchronization messages, which usually gives the best latency.
Network requirements:
- UDP communication is allowed between devices on the same subnet.
- TCP communication is allowed between devices on the same subnet.
- Network equipment does not isolate clients from each other.
- All devices in the group have stable local connectivity.
What P2P sync gives you:
- frame-accurate-like precision of the synchronization,
- real-time, low-latency device-to-device communication,
- dynamically adjusted latency compensation that levels out network latency,
- playback through the optimized native video player on each supported platform, for an instant video start,
- no additional server or device to operate.
P2P sync is suitable for Ethernet and Wi-Fi networks, but the quality of synchronization depends on the quality of the local network. For video walls and other precise playback use cases, Ethernet is preferred.
Use P2P sync when:
- all synchronized devices are in the same venue and subnet,
- your network allows device-to-device traffic,
- you want the lowest practical latency,
- you do not want to operate a local server.
Sync Server​
Sync Server is a WebSocket-based synchronization service used by devices when the selected synchronization engine is server-based. It keeps devices in the same sync group aware of each other and coordinates group state.
Historically, applets could omit syncServerUri and use the signageOS-hosted Applet Synchronizer service. That hosted service is deprecated and shut down, so new server-based deployments should provide a Synchronizer URL explicitly.
Use Sync Server mode when:
- devices cannot communicate with each other directly,
- the network blocks local UDP/TCP peer traffic,
- devices are not on the same subnet,
- your deployment architecture requires a central sync endpoint,
- you want to place a local Synchronizer close to the devices.
Deprecated cloud-hosted Sync Server​
The original cloud-hosted Applet Synchronizer service was hosted and managed by signageOS, but it is no longer the recommended deployment path. It was deprecated in favor of P2P synchronization and customer-managed Synchronizer deployments.
See the Hosted Applet-Synchronizer Service deprecation notice for the official timeline and shutdown details.
You may still see older examples that omit the server URL:
await sos.sync.connect();
Do not rely on that pattern for new server-based deployments. Configure either p2p-local synchronization or an explicit on-premise Synchronizer URL:
const syncServerUri = sos.config.sync_server || undefined;
const syncEngine = sos.config.sync_engine || 'p2p-local';
if (syncServerUri) {
await sos.sync.connect({
engine: 'sync-server',
uri: syncServerUri,
});
} else {
await sos.sync.connect({
engine: syncEngine,
});
}
If your deployment still depends on the hosted service, migrate to P2P sync or to a customer-managed Synchronizer before changing production applets.
On-premise Sync Server​
You can deploy an on-premise Synchronizer closer to the devices when you need lower latency, better resilience against internet outages, or a deployment-specific sync endpoint.
The public on-premise package is @signageos/applet-synchronizer-public.
For the standalone installation article, see How to install Synchronizer on Premise.
Requirements:
- Linux, Windows, or macOS machine reachable from all synchronized devices
- Node.js
- WebSocket communication allowed between devices and the Synchronizer host
Install the package:
npm pack @signageos/applet-synchronizer-public
Start the Synchronizer on port 8080:
PORT=8080 npm start
Then configure your applet to use the local server:
const syncServerUri = 'http://192.0.2.10:8080';
await sos.sync.connect(syncServerUri);
The local Synchronizer must be reachable from every device in the sync group. Use a stable local IP address or DNS name and make sure firewalls, VLANs, and Wi-Fi client isolation do not block WebSocket traffic.
Monitoring the on-premise Synchronizer​
The Synchronizer reports its own state, so you do not have to infer the health of a sync group from what the screens are doing. All endpoints are served on the same HTTP port as the service.
| Endpoint | Use |
|---|---|
GET /status | Liveness and dependency health. Use it for uptime probes. |
GET /dashboard/view | Human-readable HTML overview, refreshed every 2 seconds. The first place to look during an incident. |
GET /dashboard | The same snapshot as JSON: groups, connected peers, elected master, pending barriers, counters, drop rate and latency. |
GET /metrics | Prometheus exposition, prefixed with sos_applet_synchronizer_. |
Every endpoint except /status requires an Authorization: Bearer <token> header matching the Synchronizer's configured dashboard_token. Until that token is set, these endpoints respond with 404.
What to watch​
sync_drop_rate- the share of finished barriers that never confirmed, from 0 to 1. A rising drop rate means devices are not all reaching the barrier.barrier_fill_ms_last,_avg,_max- how long a barrier takes to fill, from the first device's request until the group is released.connected_peers{group}andpending_sync_requests{group}- in a healthy group these converge on every tick. Pending staying below the peer count means a device is not arriving.group_duplicate_identification{group}- two devices joined the group under the same device identification, which makes master election ambiguous.
Barrier fill latency is measured on the server: it is how long the Synchronizer waited for every device to arrive, not the playback skew between screens. Screen-to-screen alignment depends on the network fan-out to each device and has to be measured on the devices themselves.
Logs​
The Synchronizer logs through the debug package. Warnings appear without any extra configuration; for verbose output, start it with:
DEBUG=@signageos/* npm start
Two warnings are worth watching for:
barrier for group <group> timed out after <n>ms; releasing <k> pending peer(s)- a barrier was aborted because not every device arrived in time.duplicate deviceIdentification "<id>" joined group <group> (ambiguous master election)- two devices share a device identification.
Master device concept​
Every sync group has one current master device. The master coordinates the value used by the group, while the other devices follow that group state. This is why examples call sos.sync.wait(value, groupName) before playback: devices wait until the group agrees which value should play next.
The master can change over time when devices connect, disconnect, or recover. Current synchronization components choose the master deterministically based on device identification order, so all devices in the group can arrive at the same decision.
You can inspect sync status from your applet:
sos.sync.onStatus((status) => {
console.log('Connected peers:', status.connectedPeers);
console.log('Current device is master:', status.isMaster);
console.log('Sync group:', status.groupName);
});
If your logic needs to run only once per group, gate it with master status:
const isMaster = await sos.sync.isMaster(syncGroup);
if (isMaster) {
// Run group-level orchestration here.
}
Do not assume a specific physical display will always be master. If a particular screen needs different content, configure that separately, for example with a placement applet configuration value.
Synchronization accuracy and resilience​
The Sync Server releases every device in a group at the same moment, but each device receives that release after its own network latency, so screens switch within roughly one network round-trip of each other (typically well under 100 ms on a healthy network). The barrier does not carry a shared clock, so do not try to tighten this by scheduling playback against each device's wall clock - device clocks are often not tightly synchronized, and that makes the switch less aligned, not more. Act on the sos.sync.wait() release instead. If you run an on-premise Synchronizer, you can see how long a group actually takes to align in the Synchronizer's own metrics.
Failover is automatic. When the master device disconnects, the remaining devices re-elect a new master (still deterministically, by device identification order), and an in-flight sos.sync.wait() completes for the devices that are still connected - so the group keeps running instead of stalling on the device that left.
Make your wait loop resilient. Pass a timeout to sos.sync.wait(value, groupName, timeoutMs) and retry on failure:
while (true) {
try {
const value = await sos.sync.wait(nextValue, syncGroup, 15000);
// ...render the agreed value...
} catch (error) {
// A transient hiccup must not kill the loop: if this applet stops calling wait(),
// the device stays connected but never reaches the barrier and can stall the group.
console.error('sync tick failed, retrying:', error);
}
}
Implementation flow​
Most synchronized applets follow this flow:
- Wait for
sos.onReady(). - Read configuration such as
sync_group,sync_server,sync_engine, and screen placement. - Cache media to offline storage before playback.
- Connect to synchronization.
- Join the sync group.
- Wait for the group value before each playback transition.
- Prepare the next media item while the current one is playing.
Example:
import sos from '@signageos/front-applet';
sos.onReady().then(async () => {
const syncGroup = sos.config.sync_group || undefined;
const syncServerUri = sos.config.sync_server || undefined;
const syncEngine = sos.config.sync_engine || undefined;
if (syncServerUri) {
await sos.sync.connect({
engine: 'sync-server',
uri: syncServerUri,
});
} else if (syncEngine) {
await sos.sync.connect({
engine: syncEngine,
});
} else {
await sos.sync.connect();
}
await sos.sync.joinGroup({
groupName: syncGroup,
});
const videos = [
{
uid: 'video-1.mp4',
uri: 'https://example.com/video-1.mp4',
},
{
uid: 'video-2.mp4',
uri: 'https://example.com/video-2.mp4',
},
];
for (const video of videos) {
const { filePath } = await sos.offline.cache.loadOrSaveFile(video.uid, video.uri);
video.filePath = filePath;
video.arguments = [
filePath,
0,
0,
document.documentElement.clientWidth,
document.documentElement.clientHeight,
];
}
let currentVideoIndex = 0;
let previousVideo;
while (true) {
const expectedVideo = videos[currentVideoIndex];
const syncedVideoUid = await sos.sync.wait(expectedVideo.uid, syncGroup);
const syncedVideo = videos.find((video) => video.uid === syncedVideoUid) || expectedVideo;
await sos.video.play(...syncedVideo.arguments);
if (previousVideo) {
await sos.video.stop(...previousVideo.arguments);
}
const endedPromise = sos.video.onceEnded(...syncedVideo.arguments);
const nextVideoIndex = (videos.indexOf(syncedVideo) + 1) % videos.length;
await sos.video.prepare(...videos[nextVideoIndex].arguments);
await endedPromise;
previousVideo = syncedVideo;
currentVideoIndex = nextVideoIndex;
}
});
For new applets, prefer a stable sync_group value configured per deployment. Devices only synchronize with other devices in the same group.
Video wall implementation​
For video walls, synchronization only handles timing. Your content still needs to be prepared for the physical layout.
Recommended workflow:
- Produce source content at the full canvas size of the video wall.
- Slice the source content into one file per display.
- Account for bezels and display orientation during slicing.
- Assign each device a placement value, such as
left,right,top-left, orbottom-right. - Cache the placement-specific media on each device.
- Use the same sync group for every display in the wall.
- Use
sos.sync.wait()before each transition so every display starts the matching slice together.
For example, the two-screen video wall applet chooses the media list based on configuration:
const videos = sos.config.placement === 'right' ? videosRight : videosLeft;
The sync value should identify the logical content item, not the physical file path. Each screen can then map that value to its own local slice.
Supported devices​
Synchronization works on supported devices except:
- LG webOS 1
- LG webOS 2
- Samsung SSSP2
- Samsung SSSP3
For best results, synchronize the same device type and generation within one group. Mixed hardware can work, but differences in native video players, decoding, and network drivers can affect precision.
Choosing the right architecture​
| Question | Recommendation |
|---|---|
| Devices are on the same subnet and local peer traffic is allowed | Use P2P sync. |
| Devices cannot reach each other directly | Use Sync Server. |
| You want no infrastructure to manage | Use P2P sync when the local network supports it. The signageOS-hosted Applet Synchronizer service is deprecated. |
| Internet connectivity is unreliable or latency-sensitive | Deploy an on-premise Sync Server. |
| You need the best possible precision for a local video wall | Prefer P2P sync over Ethernet, or an on-premise Sync Server if P2P is blocked. |
Troubleshooting​
| Symptom | What to check |
|---|---|
| Devices do not join the same group | Verify sync_group is identical on all devices. |
| Cloud Sync Server works but P2P does not | Check UDP/TCP peer traffic, subnet boundaries, VLANs, and Wi-Fi client isolation. |
| P2P works in a lab but not at customer site | Ask network administrators about client isolation, multicast/broadcast filtering, and firewall rules. |
| On-premise Sync Server cannot be reached | Verify server IP, port, WebSocket access, and firewall rules. |
| One display plays different content | Verify placement config and that each screen maps sync values to the correct media slice. |
| Playback drifts or starts late | Cache media offline before playback and prepare the next video before the current one ends. |
| Screens fall out of step intermittently | On the Synchronizer, check sync_drop_rate and look for groups on /dashboard/view where pending requests stay below the connected peer count. |
| Screens switch late or unevenly | Check barrier_fill_ms_avg and _max for the group. One slow device holds the whole barrier, because the group is released only once every device has arrived. |
| Master device keeps changing or behaves unpredictably | Check group_duplicate_identification. Two devices sharing a device identification make master election ambiguous. |
Example applets​
- Video Sync - synchronized playlist of videos.
- Sync Mixed Content - synchronized videos and images.
- Videowall on 2 screens - horizontal two-screen video wall.
- Videowall on 4 screens - four-screen video wall.
- Synced Playlist - a playlist of videos and images played in sync across a group.
- Synced Takeover Menuboard - a menuboard that all screens interrupt together, on a shared schedule, with a synchronized full-screen takeover.
- Sync Demo - a minimal visualizer (lock-step counter + color) to check that a group is in sync.