Skip to content

Razer WYVRN

window.host

Chroma RGB lighting and Sensa HD haptics, driven by naming an event. What that event looks and feels like is decided by the WYVRN configurations installed on the viewer's machine, not by SE.Live.

The integration is optional and Windows-only. It needs Razer Synapse 4 and the Chroma App, which most OBS users do not have.

  • On Windows, every failure path ends in "unavailable" — the calls below still answer normally, they simply report that nothing is there. Check getHostCapabilities before assuming otherwise.
  • On macOS the integration is not built. None of the calls on this page are registered, getHostCapabilities is not registered either, and the host reports API version 6.7. Check the API version before calling anything here.

Initialization is asynchronous and takes about 3.4 seconds. It never blocks OBS start, so for the first few seconds of a session the status is initializing and setRazerWyvrnEvent returns false. Call getHostCapabilities first, then subscribe to hostRazerWyvrnStatusChanged — a page that only subscribes will miss the event entirely if it loads after initialization has already finished.

SE.Live ships no WYVRN configuration of its own. Anything that renders does so through a configuration some other application installed, which Synapse distributes automatically. This is by design, not a defect.

A Chroma App higher in Synapse's App Priority List takes the device over. When the viewer streams a game that is integrated with Chroma Apps, that app's lighting and haptics win and SE.Live's stop being rendered. That is Synapse arbitrating between applications rather than anything failing here, and nothing on this page reports it -- status is a closed vocabulary with no value for it, so a page cannot tell a pre-empted effect from one that rendered.

getAllRazerWyvrnEvents(ResultCallback<RazerWyvrnEventInfo[]>)

Available since API version 6.8

Every event declared by every WYVRN configuration installed on this machine.

Each entry carries its Chroma and haptic components along with session-signed URLs for their assets, so one call answers both "what can I fire" and "what would firing it do". There is no need for a second call to inspect an event.

Returns an empty array when the integration is unavailable — never an error.

Filtering. A machine with Synapse installed typically declares around 4,000 events, so an optional filter object is accepted:

window.host.getAllRazerWyvrnEvents(
    { group: '007 First Light', idPrefix: 'Aim_' },
    function (events) { /* ... */ });
Property Type Description
group string Match the event's group exactly, case-insensitively. Omit or leave empty for all.
idPrefix string Match the beginning of the event id, case-insensitively. Omit or leave empty for all.
components bool Include each event's components and asset URLs. Defaults to true.

Cost

components is not a cosmetic flag. Including them costs one filesystem probe and one URL signature per component, and the whole request runs inside the process-wide API lock — so a slow call makes OBS unresponsive, not just this caller.

Measured on a machine with Synapse installed (4,044 events, 24,698 components):

Call Time Payload
Filtered by group, with components ~50 ms 0.13 MB
Unfiltered, components: false ~65 ms 0.28 MB
Unfiltered, with components ~2.1 s 7.7 MB

A group is the configuration folder an event was declared in — the subfolder of hapticFolders that Synapse installed, named after the application it came with (007 First Light, GenericEvent). It is not part of an event's identity, since two applications may declare the same id, but it is what makes a 4,000-entry list browsable: group the ids-only sweep by group, then call back filtered by the one the user picked.

Filter, or turn components off. Asking for all 4,044 events with their components is supported and correct, but it is a two-second request returning nearly eight megabytes, and nothing else can call the host API while it runs. The intended shape is a cheap ids-only sweep followed by a filtered call for whatever the user actually selected.

Data structures: RazerWyvrnEventInfo

setRazerWyvrnEvent(RazerWyvrnEventInfo, ResultCallback<success>)

Available since API version 6.8

Fire an event.

Takes the whole RazerWyvrnEventInfo object rather than a bare string, so an item obtained from getAllRazerWyvrnEvents can be handed back unmodified; only id and fallback are read. A bare string is also accepted.

id is matched case-insensitively against the scanned configurations, and the event is fired under the spelling the configuration uses — so aim_on reaches the SDK as Aim_On.

An id nothing declares is still sent, exactly as you spelled it. The scan decides the spelling, not whether the call is allowed: it is a cache of what was on disk when it ran, so a configuration installed since then is invisible here and would otherwise be permanently unreachable. The SDK is left to judge.

Stopping playback

Three spellings mean the same thing, because a caller clearing an event should not have to remember which shape the API wanted:

window.host.setRazerWyvrnEvent(null, cb);       // null
window.host.setRazerWyvrnEvent(cb);             // no argument at all
window.host.setRazerWyvrnEvent({ id: '' }, cb); // an empty id

fallback

fallback names another event to try when this one is not declared by any configuration on the machine. It takes the same shape and nests to arbitrary depth:

window.host.setRazerWyvrnEvent({
    id: 'Headshot',
    fallback: { id: 'Hit',
                fallback: { id: 'Generic_Impact' } }
}, cb);

The first entry in the chain that names a scanned event wins, and only that one is fired. If nothing in the chain is declared on this machine, the first id is sent as given — the chain that was tried is written to the OBS log, so a name that renders nothing is still visible to whoever asks why.

The chain is resolved against the scanned configurations, not against the SDK. That is deliberate: CoreSetEventName accepts an event belonging to a different application and reports success, so asking the SDK "did that work?" would always answer yes and the fallback would never fire.

Nesting is bounded at 16 levels, since the whole call runs inside the process-wide API lock.

Return value

Returns false when the integration is not ready — including during the first few seconds of a session, while initialization is still running, and once shutdown has begun. It does not report whether the event rendered: an unknown id is passed to the SDK and returns true, and the SDK accepts names belonging to other applications without rendering them.

Rate limiting is newest-wins. The SDK accepts at most 30 events per second. An event arriving inside that window is parked rather than dropped, and if another arrives before the parked one is sent, it replaces it. The backlog is one item by construction, and the name that survives is always the most recent — the last thing that happened on the stream is the thing worth rendering. A superseded event is logged to the OBS log so a name that never rendered is visible rather than silently absent.

Data structures: RazerWyvrnEventInfo

Enum values

Every closed vocabulary this API returns is camelCase, or plain lowercase where the value is a single word. The configurations on disk use the vendor's own capitalisation (Chest, VeryHigh, ChromaLink); it is normalised on the way out, by lowercasing the first character and leaving the rest.

Field Values
RazerWyvrnChromaComponent.device keyboard, keyboardExtended, keypad, mouse, mousepad, headset, chromaLink
RazerWyvrnHapticComponent.mixing merge, override
RazerWyvrnHapticComponent.priority veryLow, low, medium, high, veryHigh
RazerWyvrnHapticTarget.target hand, head, chest, waist, leg, all, down, top
RazerWyvrnHapticTarget.spatialization global, left, right
RazerWyvrnEventInfo.kind exact, fallbackPattern
RazerWyvrnStatus.status see RazerWyvrnStatus

These are the values observed across every configuration installed on a machine with Synapse. Treat the lists as complete but not closed — a future configuration may introduce a value not listed here, and it will arrive normalised the same way rather than being dropped.

Normalisation folds case splits in the source data onto one value: both Waist and waist occur in the wild and both arrive as waist. It does not map unknown spellings onto known ones — the shipped data contains a Wasit typo, which arrives as wasit rather than being silently corrected into waist, because a caller cannot otherwise tell a real value from a repaired one.

Rendering previews

The asset URLs returned by getAllRazerWyvrnEvents point at the local file server and are session-signed; fetching one yields the exact bytes on disk. Both formats decode entirely in the browser.

A URL with a missing or altered signature is refused with { "success": false, "message": "Invalid Request Signature" }, and the file is not served.

The .chroma format

A frame-by-frame colour animation. All integers are little-endian.

uint32  version
uint8   deviceType       0 = 1D strip, 1 = 2D grid
uint8   device
uint32  frameCount
repeat frameCount times:
    float32  duration    seconds
    uint32   colors[ledCount]

Colours are COLORREF — 0x00BBGGRR, so the low byte is red, not blue.

ledCount is not in the file. It comes from the (deviceType, device) pair, which is the one thing a decoder cannot discover from the asset itself:

Device deviceType device Colors per frame Grid
ChromaLink 0 0 5 5 LEDs
Headset 0 1 5 5 LEDs
Mousepad 0 2 15 15 LEDs
Keyboard 1 0 132 6 × 22
Keypad 1 1 20 4 × 5
Mouse 1 2 63 9 × 7
Keyboard (extended) 1 3 192 8 × 24

device indexes within its type class, so the number alone is ambiguous — the pair is the key.

Both keyboard geometries occur under the same _Keyboard filename, and only the header separates them. Assuming the 192-LED grid parses the 132-LED files without error and renders as noise, so verify the size rather than trusting the table:

size === 10 + frameCount * (4 + ledCount * 4)

Refuse a mismatch.

The .haps format

JSON, in one of two schemas: a plain one (vibration.melodies[].notes[]) and an m_-prefixed one (m_vibration.m_melodies[].m_notes[]). Both occur in the shipped data.

They are not simply renamed. In the m_ schema, amplitude is normalised to 0..1 but frequency is absolute Hz, where the plain schema normalises both. Treating one as the other yields frequencies three orders of magnitude wrong.

Transients (transients[] / m_transients[]) sit alongside melodies and carry their own amplitude; a decoder that walks only the melodies reports zero amplitude for a transient-only file.

Events

hostRazerWyvrnStatusChanged