Open Streamer

May 26, 2026 · View on GitHub

Domain events emitted on the in-process event bus and delivered to registered hooks. Every state change worth alerting on or auditing fires an event; consumers subscribe via hook configuration or — for in-process integrations — via events.Bus.Subscribe.

For the data-flow / hooks delivery contract see ARCHITECTURE.md § Event Bus & Hooks.


1. Envelope

Every event carries the same shape:

type Event struct {
    ID         string         // UUID — idempotent delivery key
    Type       EventType      // see catalogue below
    StreamCode StreamCode     // empty for system-level events (config / hooks meta)
    OccurredAt time.Time      // server-side wall clock at publish
    Payload    map[string]any // event-specific fields, see "Payload" column
}

When delivered over HTTP the bus serialises the event to JSON inside an array (HTTP hooks ship batches; file hooks ship one JSON object per line). Hooks layer adds an optional metadata map merged into Payload from the operator-configured per-hook fields — useful for routing keys.


2. Catalogue

2.1 Stream lifecycle

TypeEmitterTriggers whenPayload
stream.createdAPI handler (stream.go)POST /streams/{code} succeeds with a fresh codeempty
stream.updatedAPI handler (stream.go)POST /streams/{code} succeeds on an existing codewas_running: bool, now_enabled: bool, disabled: bool
stream.startedCoordinator (coordinator.go)Pipeline goes from stopped → running (Start succeeds)empty
stream.stoppedCoordinator (coordinator.go)Pipeline goes from running → stopped (Stop / shutdown)empty
stream.deletedAPI handler (stream.go)DELETE /streams/{code} succeedsempty
stream.runtime_createdAutopublish (service.go)A template-prefix match materialises a runtime stream (encoder pushed to a path matching a template prefix; the matched template carries a publish:// input). Runtime streams are NEVER in the on-disk repotemplate_code: string
stream.runtime_expiredAutopublish (service.go)Idle reaper stops a runtime stream after 30 s without a packet on the buffer hubtemplate_code: string

2.2 Input health

TypeEmitterTriggers whenPayload
input.connectedIngestor (service.go:220, 402)PacketReader.Open succeeds (initial or after reconnect)input_priority: int
input.reconnectingIngestor (service.go:409)Transient read error; pull worker is retryinginput_priority: int, err: string
input.degradedManager (service.go:656, 708) + CoordinatorHealth timeout OR ReportInputError triggeredinput_priority: int, reason: string
input.failedIngestor (service.go:254, 440)Pull worker exited non-retriableinput_priority: int, err: string
input.failoverManager (service.go:611, 831)Active input switched (degraded primary → backup OR failback OR manual)from: int, to: int, reason: string
input.recoveredManager (service.go:runProbe)Failback probe succeeded — previously-degraded input is healthy again. Distinct from input.failover reason="recovery"/"failback" because consumers monitoring "is the source back" want one signal, not a payload-string scrapeinput_priority: int, was_exhausted: bool

reason enum on input.failover: initial, error, timeout, manual, failback, recovery, input_added, input_removed.

2.3 Transcoder

TypeEmitterTriggers whenPayload
transcoder.startedTranscoder (service.go:408)Transcoder subprocess spawned for the streamempty
transcoder.stoppedTranscoder (service.go:494)The stream's transcoder subprocess has exited cleanlyempty
transcoder.errorTranscoder (supervisor.go) + Coordinator (coordinator.go)Transcoder subprocess crash / non-zero exitprofile: string, err: string

2.4 DVR / Recordings

TypeEmitterTriggers whenPayload
recording.startedDVR (service.go:197)A recording subscription begins (operator enables DVR or pipeline starts with dvr.enabled=true)recording_id: string
recording.stoppedDVR (service.go:236)Recording subscription ends gracefullyrecording_id: string
recording.failedDVR (service.go:443)Segment write or rotation hits a non-recoverable errorrecording_id: string, err: string
segment.writtenDVR (service.go:483)One TS segment flushed to disk + manifest updatedrecording_id: string, index: int, size_bytes: int64, duration_sec: float, discontinuity: bool
dvr.segment_prunedDVR (service.go:applyRetention)Retention loop deleted an aged-out segmentrecording_id: string, segment_index: int, size_bytes: int64, reason: "age" | "size"

2.5 Push out (RTMP / RTMPS)

Per-destination state transitions. Each event fires only on STATE CHANGE — no spam on noisy retry loops because setPushStatus short-circuits when the new status equals the previous.

TypeEmitterTriggers whenPayload
push.startedPublisher (runtime.go:publishPushEvent)Push goroutine spawns — about to dial targeturl: string
push.activePublisherlal.PushSession.Push handshake succeeded; media flowingurl: string
push.reconnectingPublisherWrite fail or input discontinuity; retryingurl: string
push.failedPublisherdest.Limit retry attempts exhausted, or terminal errorurl: string

2.6 Play sessions

TypeEmitterTriggers whenPayload
session.openedSessions tracker (tracker.go:614)New PlaySession created — first segment GET (HLS/DASH) or TCP handshake (RTMP/SRT/RTSP)session_id, proto, ip, user_agent, country, user_name
session.closedSessions tracker (tracker.go:638)TCP disconnect / idle timeout / kicked / shutdown sweepsession_id, proto, ip, bytes, duration_sec, reason

reason enum: idle, client_gone, kicked, shutdown.

2.7 Server config

TypeEmitterTriggers whenPayload
config.changedAPI handler (config.go:UpdateConfig, config_yaml.go:ReplaceConfigYAML)POST /config or PUT /config/yaml succeedssource: "post_config" | "put_config_yaml"

Note: the bulk PUT /config/yaml replace fans out into individual stream.updated / hook.updated events as well — config.changed is the umbrella audit signal for the whole transaction.

2.8 Watermark assets

TypeEmitterTriggers whenPayload
watermark.asset_createdAPI handler (watermark.go:Upload)POST /watermarks upload succeedsasset_id: string, name: string
watermark.asset_deletedAPI handler (watermark.go:Delete)DELETE /watermarks/{id} succeedsasset_id: string

2.9 Hook lifecycle (meta-events)

Audit events for the hook system itself. Useful for inventory sync / infrastructure-as-code drift detection.

TypeEmitterTriggers whenPayload
hook.createdAPI handler (hook.go:Create)POST /hooks succeedshook_id: string, hook_type: "http" | "file"
hook.updatedAPI handler (hook.go:Update)PUT /hooks/{hid} succeedshook_id: string, hook_type: string
hook.deletedAPI handler (hook.go:Delete)DELETE /hooks/{hid} succeedshook_id: string, hook_type: string (from the pre-delete record)

Beware the recursion risk: a hook.* event subscriber that creates / updates / deletes hooks will trigger more hook.* events.

2.10 Template lifecycle (meta-events)

Audit events for the template system. Updates fire AFTER every dependent stream's pipeline has been hot-reloaded so downstream consumers can assume the new resolved config is live by the time the event is delivered.

TypeEmitterTriggers whenPayload
template.createdAPI handler (template.go)POST /templates/{code} succeeds with a fresh codetemplate_code: string
template.updatedAPI handler (template.go)POST /templates/{code} succeeds on an existing code; dependent running streams have been hot-reloaded via coordinator.Updatetemplate_code: string
template.deletedAPI handler (template.go)DELETE /templates/{code} succeeds (no streams reference the template)template_code: string

3. Filtering (per-hook)

Each hook has two filter fields:

event_types:    ["stream.started", "input.failover"]   # whitelist
stream_codes:   ["news", "sports"]                     # whitelist
event_types_except: ["session.opened"]                  # blacklist
stream_codes_except: ["test*"]                          # blacklist (glob)

Empty whitelist = match all. Blacklists override whitelists when both match. Stream-code filters use path.Match glob syntax (* and ?, no **).


4. Delivery shapes

HTTP hooks

Events accumulate in a per-hook batcher; flushed when:

  • BatchMaxItems reached (default 100), OR
  • BatchFlushIntervalSec elapsed (default 1)

POST body is a JSON ARRAY of event envelopes. HMAC signs the entire body when secret is set:

X-OpenStreamer-Signature: sha256=<hex>
X-OpenStreamer-Batch-Size: <int>
Content-Type: application/json

Failed batches re-queue at the FRONT of the buffer for the next flush — chronological order preserved across retries. The buffer is bounded by BatchMaxQueueItems (default 1000); overflow drops the OLDEST events and increments open_streamer_hooks_events_dropped_total.

File hooks

Append one JSON-encoded event per line to an absolute path. Concurrent deliveries to the same path serialise via a per-target mutex. Drop-in for Filebeat / Vector / Promtail tail-and-ship pipelines.


5. Event-driven recipes

Slack alert on push failure

hooks:
  - id: slack-push-alert
    type: http
    target: https://hooks.slack.com/services/T.../B.../...
    event_types: ["push.failed"]
    metadata:
      channel: "#streaming-ops"

Audit log of every config change to S3 (via Vector sidecar)

hooks:
  - id: config-audit
    type: file
    target: /var/log/open-streamer/config-audit.jsonl
    event_types: ["stream.updated", "config.changed", "hook.created", "hook.updated", "hook.deleted"]

Auto-resolve PagerDuty on input recovery

hooks:
  - id: pd-resolve
    type: http
    target: https://events.pagerduty.com/v2/enqueue
    event_types: ["input.recovered", "stream.started"]
    secret: "${PD_ROUTING_KEY}"

6. Reference files

ConcernFile
EventType constantsinternal/domain/event.go
Bus implementationinternal/events/bus.go
Hook delivery (HTTP batcher + file sink)internal/hooks/
Per-hook subscription wiringinternal/hooks/service.go § Start
Metrics for hook deliveryMETRICS.md § Hooks