VIF ships with four built-in event listeners: Id3Event, WebhookEvent2, LogFileEvent, and OverlayEvent, covering ID3 metadata, webhook delivery, log files, and video overlays. When your integration needs to act on a detection rather than just report it (starting a recording, triggering an alarm system, writing to a database), you write a custom event listener. A custom event listener is a Java class that implements IVifEventListener and is dropped into WSE like any other plugin.
This article explains the IVifEventListener interface itself so you can build your own, and a complete walkthrough of the WSE VI intelligence event recording plugin, a real, working listener that records an MP4 clip whenever specific object classes are detected, as a worked example you can install and configure as-is, or use as a template for your own.
The IVifEventListener interface
package com.wowza.wms.plugin.videointelligence.api;
public interface IVifEventListener {
static String getVersion();
default void onInit(IApplicationInstance appInstance, IMediaStream stream,
Set<String> methods, HashMap<String, Object> properties);
default void onShutdown();
default boolean immediate(DetectionResponse response);
default boolean batch(ArrayList<DetectionResponse> responses);
default boolean rollup(DetectionResponse response);
}
All five instance methods are default. You only override the ones your listener actually uses. A listener that only cares about immediate delivery doesn't need batch or rollup overrides at all.
| Method | Called when | Purpose |
|---|---|---|
onInit(appInstance, stream, methods, properties) |
Once, when the listener is attached to a stream | Parse your configured properties, store stream/application references, set up external connections |
onShutdown() |
Once, when the listener is detached or the stream stops | Release resources, stop any recorder or timer you started |
immediate(DetectionResponse) |
Per detection response, if methods includes immediate |
Lowest-latency delivery |
batch(ArrayList<DetectionResponse>) |
Periodically, grouping responses, if methods includes batch |
Process accumulated responses together |
rollup(DetectionResponse) |
Periodically, on a rollup interval, if methods includes rollup |
Statistical summaries rather than raw values |
properties arrives as a raw HashMap<String, Object>. There's no schema enforcement at the Java layer, so your onInit is responsible for reading and type-converting each key itself.
DetectionResponse isn't one flat shape. It's a base class with type-specific subclasses (ObjectDetectionResponse, SceneDetectionResponse, VlmDetectionResponse), each carrying different fields. The full breakdown of every field on every subclass is in the companion API reference; this article sticks to what EventRecordingPlugin actually reads from it.
Worked example: EventRecordingPlugin
What it does
EventRecordingPlugin uses the WSE StreamRecorder API to capture an MP4 clip whenever a tracked object of a configured class is detected, with a pre-event back-buffer and a post-event tail, producing one continuous file per object-presence event rather than one file per individual detection.
How it works
A few internal design choices are worth understanding, both because they explain the plugin's behavior and because they generalize to listeners you'd write yourself:
- One recorder per stream, extended rather than restarted: If a new qualifying detection arrives while already recording, the plugin cancels the pending stop and keeps recording, producing one continuous file even when several different objects trigger overlapping detections, instead of many short overlapping clips.
- Wall-clock eviction, not response-cadence eviction: VIF's detection stream interleaves confirmed detections, detections with no tracking ID yet, and empty responses. The plugin tracks
System.currentTimeMillis()per object and evicts it only afterlost_object_duration_msof real elapsed time, so sparse or noisy responses don't distort when an object is considered gone. - Queuing until the recording is actually ready:
LiveStreamRecordManager.startRecording()is asynchronous; it returns before the file exists. The plugin queues its webhook events in memory and flushes them from theIStreamRecorderActionNotify.onSegmentStart()callback once the file is open and a real path is available. This generalizes to any listener side effect that depends on something else finishing asynchronously first. - Back-buffer via a keyframe walk: A recording can only start on a keyframe, so to honor the configured back-buffer duration, the plugin walks
stream.getPlayPackets()backward from the last keyframe to find the earliest keyframe within the window. - Reuses existing WSE infrastructure: Its own webhook payloads go out through the same shared
WebhookListenersingleton built-in listeners use (Server.getInstance().getProperties()), rather than opening a separate HTTP client, keeping delivery, retries, and auth consistent with the rest of yourWebhooks.jsonconfiguration. It also deliberately doesn't duplicate webhooks WSE already emits on its own (recording start/stop). Worth checking what the platform already reports before adding your own event for the same thing.
Install
The plugin is two files: the compiled JAR and a JS file that defines its Manager UI fields.
Docker Compose (VIF runtime repo). The stock docker-compose.yaml doesn't mount a location for third-party plugins by default. Create one and add matching volume mounts:
mkdir -p wse.addon/lib wse.addon/listeners cp build/libs/wse-plugin-video-intelligence-event-recording-<version>.jar wse.addon/lib/ cp src/main/js/EventRecordingPlugin.js wse.addon/listeners/
services:
wse:
volumes:
- ./wse.addon/lib:/usr/local/WowzaStreamingEngine/lib.addon
manager:
volumes:
- ./wse.addon/listeners:/usr/local/tomcat/webapps/ROOT/wse-plugins/server/vif/listeners.addon
docker compose up -d --force-recreate wse manager
If you've already added other plugins, these mounts may already exist. Just drop the files into the existing folders.
Standalone (non-Docker) WSE:
cp build/libs/wse-plugin-video-intelligence-event-recording-<version>.jar ${WSE_HOME}/lib/
cp src/main/js/EventRecordingPlugin.js ${WSEM_HOME}/temp/webapps/enginemanager/wse-plugins/server/vif/listeners
Restart both WSE and WSE Manager. (The Manager's temp/ directory may not exist yet if Manager hasn't started at least once.)
Configure
The easiest path is through the VIF Manager UI. Edit the stream's configuration, add EventRecordingPlugin under event listeners, and its fields (from the JS file installed above) appear in the form. The UI writes the result into video-intelligence.json for you.
Editing the JSON directly is only needed if you're not using the UI:
"vif_event_listeners": {
"EventRecordingPlugin": {
"class_name": "EventRecordingPlugin",
"methods": ["immediate"],
"properties": {
"class_names": ["person", "car"],
"back_buffer_time": 5000,
"post_detection_duration": 5000,
"lost_object_duration_ms": 3000,
"recording_name_template": "${SourceStreamName}_${RecordingStartTime}",
"record_stream_name": "",
"output_path": "",
"webhooks": true
}
}
}
Properties reference:
| Property | Type | Default | Description |
|---|---|---|---|
class_names |
string_list | ["all"] |
Object classes that trigger recording. "all" matches any class. |
back_buffer_time |
number | 5000 |
Milliseconds of video captured before the detection event (drawn from the WSE incoming stream buffer, ~8s available). |
post_detection_duration |
number | 5000 |
Milliseconds to keep recording after the last qualifying detection. |
lost_object_duration_ms |
number | 3000 |
Wall-clock milliseconds since an object was last seen before it's considered lost. |
recording_name_template |
text | ${SourceStreamName}_${RecordingStartTime} |
File naming template. See tokens below. |
record_stream_name |
text | (empty) | Regex matching the stream to record. Empty records the source stream; use .*-vi$ to record the VIF transcoder output with overlays. First match wins. |
output_path |
text | (empty) | Output directory. Empty uses the WSE application default. |
webhooks |
boolean | true |
Send webhooks on detection start/stop. |
File name tokens (for recording_name_template):
| Token | Example | Description |
|---|---|---|
${SourceStreamName} |
myStream | Stream name |
${BaseFileName} |
myStream | Base filename before versioning |
${RecordingStartTime} |
2026-05-01-12.00.00.000-UTC | Wall-clock time of the first frame (accounts for back_buffer_time) |
${SegmentNumber} |
0 | Segment number |
${SegmentTime} |
2026-05-01-12.00.00.000-UTC | Segment creation time |
${ClassNames} |
person_car | Underscore-joined triggering class names |
${TrackingIds} |
2_5 | Underscore-joined tracking IDs active at recording start |
Building it yourself (or from source)
dependencies {
compileOnly 'com.wowza.wms:wms-server:4.11.0'
compileOnly 'org.apache.logging.log4j:log4j-core:2.25.4'
compileOnly 'com.wowza.wms.plugin.videointelligence:wse-plugin-video-intelligence:0.15.0'
}
Dependencies are compileOnly, not implementation. WSE provides these at runtime, and bundling them would create duplicate-class conflicts. Pin the VIF plugin version to whatever your target WSE's VIF module version actually is. Use a distinct package/group, not the bare com.wowza.wms.plugin.videointelligence namespace. That package already contains classes in the VIF library JAR itself, and a same-named class in your plugin would be silently shadowed by VIF's version at runtime.
The reference repo builds inside a wowza/wse-plugin-builder Docker image (see build.sh for the exact invocation), which supplies the WSE library JARs at compile time. Without Docker, point Gradle at a local WSE installation's lib/ directory instead. Either way, the compiled JAR lands in build/libs/.
Best practices when building a custom listener
- Pick your delivery mode:
immediategives the lowest latency but fires per response.batch/rolluptrade latency for fewer, aggregated calls, better for summary-style integrations than anything time-critical. - Don't key state transitions off response cadence: Use wall-clock elapsed time since last-seen, the way
EventRecordingPlugindoes for object loss. - Watch for async WSE APIs: If your side effect depends on something else completing (a file path, a connection handle), queue and flush from the relevant completion callback rather than assuming it's ready immediately after the call returns.
- Reuse existing WSE infrastructure where you can: rather than reimplementing delivery, retries, and auth your integration doesn't need to own.
Note: The complete, buildable source, including the file-naming delegate (
EventRecordingFileVersionDelegate.java) and the full design document with the keyframe-walk algorithm and webhook payload schemas, is at wse-plugin-video-intelligence-event-recording repo.
See also
- Configure VIF in Wowza Streaming Engine (event listener configuration,
vif_event_listenersschema) - Configure Webhooks for VIF (the webhook delivery infrastructure this plugin builds on)
- Access VIF logs in WSE (the
DetectionResponsefields available to your listener)




