LiveStreamEndedPayload live.stream.ended@1

Generated at from openvibe-contracts v0.33.0 and openvibe-sdk v0.5.0.

Version
1.0.0
Owner
live
Visibility
public
Status
active
Compatibility
backward
Decision
ADR-004
Schema
https://openvibe.network/contracts/events/payloads/live.stream.ended.v1.json

live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.

Fields

FieldTypeRequiredDescriptionConstraints
stream_idintegeryesLive's streams row id (the envelope subject id).
  • minimum 1
channelobjectyes
  • no other fields
channel.usernamestringyesThe streamer's Live username (the channel slug).
  • minLength 1
channel.display_namestringyesThe display name, or the username when there is none.
  • minLength 1
channel.urlstringyesThe channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit.
  • pattern ^https://openvibe\.live/
  • format uri
channel.subjectobjectThe streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill).
  • no other fields
channel.subject.typeconstyes
  • = "user"
channel.subject.idstringyes
  • pattern ^usr_[0-9A-HJKMNP-TV-Z]{26}$
titlestringyes
categorystring | nullyesnull when the streamer chose none (the AI classifies the stream from what it shows).
protocolenumyesHow the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp).
  • one of "jsmpeg", "webrtc", "rtmp"
is_nsfwbooleanyes
started_atstring | nullyesUTC. null only for a row without a start time, which createStream never writes.
  • format date-time
ended_atstringyesUTC.
  • format date-time
duration_secondsinteger | nullyesWhole seconds from started_at to ended_at; null when the row has no start time.
  • minimum 0

Examples

From the contract's own test fixtures: valid ones validate, rejected ones must fail.

Valid: ended
{
  "stream_id": 4211,
  "channel": {
    "username": "alice",
    "display_name": "Alice",
    "url": "https://openvibe.live/@alice",
    "subject": {
      "type": "user",
      "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
    }
  },
  "title": "Morning walk through Shinjuku",
  "category": "irl",
  "protocol": "webrtc",
  "is_nsfw": false,
  "started_at": "2026-09-23T18:02:11.000Z",
  "ended_at": "2026-09-23T19:02:41.000Z",
  "duration_seconds": 3630
}
Valid: no-start-time
{
  "stream_id": 4211,
  "channel": {
    "username": "alice",
    "display_name": "Alice",
    "url": "https://openvibe.live/@alice",
    "subject": {
      "type": "user",
      "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
    }
  },
  "title": "Morning walk through Shinjuku",
  "category": "irl",
  "protocol": "webrtc",
  "is_nsfw": false,
  "started_at": null,
  "ended_at": "2026-09-23T19:02:41.000Z",
  "duration_seconds": null
}
Rejected: subject-not-a-user
{
  "stream_id": 4211,
  "channel": {
    "username": "alice",
    "display_name": "Alice",
    "url": "https://openvibe.live/@alice",
    "subject": {
      "type": "service",
      "id": "live"
    }
  },
  "title": "Morning walk through Shinjuku",
  "category": "irl",
  "protocol": "webrtc",
  "is_nsfw": false,
  "started_at": "2026-09-23T18:02:11.000Z",
  "ended_at": "2026-09-23T19:02:41.000Z",
  "duration_seconds": 3630
}
Rejected: without-ended-at
{
  "stream_id": 4211,
  "channel": {
    "username": "alice",
    "display_name": "Alice",
    "url": "https://openvibe.live/@alice",
    "subject": {
      "type": "user",
      "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
    }
  },
  "title": "Morning walk through Shinjuku",
  "category": "irl",
  "protocol": "webrtc",
  "is_nsfw": false,
  "started_at": "2026-09-23T18:02:11.000Z",
  "duration_seconds": 3630
}

Validate

const contracts = require('openvibe-contracts');
contracts.validate('live.stream.ended@1', value);   // { valid, errors: [{ path, message }] }