Besides the Unity Crossfire Plugin, Namazu Crossfire ships two Java-based client libraries that speak the same wire protocol: a JVM/native client for desktop, server-to-server, and integration-test use, and a browser client compiled to JavaScript via TeaVM. Both sit on top of a small shared API so Application code can be written mostly against interfaces rather than against a specific WebRTC backend.
The shared client API #
The client module defines the vendor-neutral surface both implementations expose:
Crossfire— the top-level facade. Obtain a builder from aCrossfireClientProvider(an SPI, discovered viaMETA-INF/services), configure it, thenconnect(). Modes are combinations ofProtocol(WebRTCorSIGNALING) and role (host or client) — e.g.WEBRTC_HOST. After connecting, callfindMatchHost()/findMatchClient()(optionally scoped to a specificProtocol) to get the active peer collection, and subscribe toonHostOpenStatus/onClientOpenStatusfor open/close notifications.SignalingClient— the underlying handshake/signal transport. ExposesgetState()(match id, host, Profile roster),backlog()(the buffered signals described in the protocol reference),signal(Signal),control(ControlMessage), and threehandshake(...)overloads (fire-and-forget, callback, or blocking with a timeout). Its own phase enum mirrors the server’s connection state machine:READY → CONNECTED → HANDSHAKING → SIGNALING → TERMINATED.MatchHost—start(),knownPeers(),findPeer(profileId),newPeerQueue(),onPeerStatus(...),close().MatchClient—connect(),findPeer()(there’s only one — the host),newPeerQueue(),onPeerStatus(...),close().Peer— one connected participant.getPhase()(READY/CONNECTED/TERMINATED),send(String)/send(ByteBuffer)(returns aSendResult:SENT/NOT_READY/ERROR/TERMINATED), andonMessage/onStringMessage/onErrorsubscriptions.
Implementation-agnostic configuration is expressed as plain records, so the same values can be handed to either backend:
CrossfireIceServer—urls,username,password,hostname,tlsCertPolicy.CrossfireIceServer.googleDefaults()returns Google’s public STUN servers.CrossfireDataChannelConfig—ordered,negotiated,maxPacketLifeTime,maxRetransmits,id,protocol.defaults()is ordered, non-negotiated, with no lifetime/retransmit caps.CrossfireOfferOptions—voiceActivityDetection,iceRestart.defaults()has VAD on, ICE restart off.CrossfireTlsCertPolicy—SECUREorINSECURE_NO_CHECK, for relaxing certificate validation against a local dev TURN/relay server using a self-signed certificate.
Both concrete implementations extend AbstractCrossfire, which handles the common bookkeeping: it subscribes to the SignalingClient‘s signals, and on receiving a HOST signal it works out — per supported protocol — whether the local peer is the host or a client, then builds a fresh set of MatchHost/MatchClient instances via two hooks the subclass supplies (populateHosts/populateClients). Application code normally never touches AbstractCrossfire directly.
JVM client (client-onvoid) #
Built on dev.onvoid.WebRTC:WebRTC-java (native WebRTC bindings) plus the Jakarta WebSocket client API for the signaling transport. This is the client to reach for in a JVM game server, a headless bot, or an integration test — not for a browser deployment.
Get a builder from OnvoidCrossfireClientProvider:
Crossfire crossfire = new OnvoidCrossfireClientProvider()
.newBuilder()
.withDefaultUri(URI.create("wss://your-server/app/ws/crossfire"))
.withIceServers(List.of(CrossfireIceServer.googleDefaults()))
.withDataChannelConfig(CrossfireDataChannelConfig.defaults())
.build();
crossfire.connect();
MatchHost host = crossfire.findMatchHost().orElseThrow();
If no URI is supplied, the builder falls back to the ELEMENTS_CROSSFIRE_URI environment variable, then the dev.getelements.elements.crossfire.client.uri system property.
Host role: WebRTCMatchHost listens for CONNECT/DISCONNECT signals and lazily creates one offering peer (WebRTCOfferingPeer) per remote Profile as they connect. Client role: WebRTCMatchClient wraps a single answering peer (WebRTCAnsweringPeer) targeting whichever Profile the HOST broadcast signal names. Both share one native PeerConnectionFactory (SharedPeerConnectionFactory) and a single serialized executor for native WebRTC calls (SharedWebRTCExecutor) by default — override with withPeerConnectionFactory/withExecutor only if you need per-instance isolation.
CrossfireIceServer, CrossfireOfferOptions, and CrossfireDataChannelConfig map onto onvoid’s native RTCIceServer/RTCOfferOptions/RTCDataChannelInit types, including the TLS cert policy. Note that offer options only flow to the host (offering) side — the answering side always uses onvoid’s default RTCAnswerOptions.
Browser client (client-teavm) #
Compiles the same client abstractions to JavaScript using TeaVM, calling the browser’s native WebSocket and RTCPeerConnection directly through thin JSO overlays — no separate JS SDK to keep in sync with the Java protocol model. Get a builder from TeaVMCrossfireClientProvider; usage mirrors the JVM client:
Crossfire crossfire = new TeaVMCrossfireClientProvider()
.newBuilder()
.withDefaultUri(URI.create("wss://your-server/app/ws/crossfire"))
.withIceServers(List.of(CrossfireIceServer.googleDefaults()))
.build();
crossfire.connect();
The host/client peer roles work the same way as the JVM client (an offering peer per remote Profile on the host side, one answering peer on the client side), backed by plain HashMaps rather than concurrent collections, since generated JS is single-threaded.
Known gaps versus the JVM client — worth knowing before you assume feature parity:
withOfferOptions(...)andwithDataChannelConfig(...)are accepted by the builder but currently have no effect on the browser target — data channels are always created with a hardcoded{"ordered":true}configuration. Don’t rely on custom retransmit/lifetime/negotiated settings in a browser build.CrossfireIceServer.hostnameandtlsCertPolicyare silently dropped when building the browser’s ICE server list — browsers manage their own TLS trust and don’t expose a knob to override it, soCrossfireTlsCertPolicyhas no meaning here.newPeerQueue()throwsUnsupportedOperationExceptionon bothMatchHostandMatchClientin the browser build — a blocking queue would deadlock a single-threaded JS runtime. UseonPeerStatus(...)callbacks instead of polling a queue.
The client-teavm module pins an older Jetty version for its test/dev browser runner and excludes the Elements SDK BOM’s newer Jetty transitives to avoid a classpath conflict — if you fork this module’s build, keep that exclusion, or the embedded test server won’t start.
Choosing a client #
| Target | Module | Use for |
|---|---|---|
| Unity (desktop/console/mobile) | Unity Crossfire Plugin | Shipping games built on Unity Netcode for GameObjects |
| JVM (native WebRTC) | client-onvoid | Headless bots, load/integration tests, JVM-based game servers acting as a peer |
| Web browser | client-teavm | Browser-based game clients, browser-side test harnesses |

