Create a custom event listener in Wowza VIF

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 after lost_object_duration_ms of 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 the IStreamRecorderActionNotify.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 WebhookListener singleton built-in listeners use (Server.getInstance().getProperties()), rather than opening a separate HTTP client, keeping delivery, retries, and auth consistent with the rest of your Webhooks.json configuration. 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: immediate gives the lowest latency but fires per response. batch/rollup trade 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 EventRecordingPlugin does 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