GlimpseEngine · Subtitle Integration · V1

Live Subtitle Integration Guide

You publish one live video stream over SRT. We watch it, write football commentary, and return each line to you as text with the media timestamp it describes. You synthesise and play the audio yourself.

HTTPS + WebSocketSRT + MPEG-TSMulti-language textUpdated 2026-09-05

1. Delivery surfaces

Two surfaces carry an identical payload and share one sequence space, so you can develop against polling and move to WebSocket without changing how you handle a line.

SurfaceEndpointUse when
PollingGET /v1/matches/{match_id}/subtitlesSimplest to integrate and debug. Start here.
WebSocketwss://…/v1/matches/{match_id}/subtitles/streamLowest latency. Switch before production if the polling interval matters to you.
Not in this release: a push callback. If you send subtitle_callback when arming a match, the API returns 400 and tells you to use polling or WebSocket. The feature is absent, not unreliable.

2. Credentials

Provisioning issues these separately. They are not interchangeable, and you need both of the first two: one to send the match in, one to read the commentary back out.

CredentialOpensNotes
Match tokenCreate, arm, end and read your own matchesServer-to-server. Self-service — no operator involvement.
Commentary stream tokenRead subtitles (polling and WebSocket)Read-only. This is the one your subtitle consumer uses.
Event tokenPOST /v1/eventsIssued only if you have an official match data feed. See §6.
SRT publish URLVideo contributionReturned by arm, contains a secret stream id. Treat the whole URL as a password.

All HTTPS requests use Authorization: Bearer <token>. Base URL: https://live.nextmoment.ai.

The most common first-day mistake. Using the match token to read subtitles returns 401. The match token does not open that route and never will. If your stream publishes fine and you receive nothing, check which token your consumer is sending.

3. Create the match, and declare your languages

POST/v1/matches

This is where you choose which languages you receive. It cannot be changed after the match is created.

curl -X POST 'https://live.nextmoment.ai/v1/matches' \
  -H 'Authorization: Bearer <match-token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "match_id": "bundesliga-2026-001",
    "match_package": { "...": "team and squad information, supplied separately" },
    "output": {
      "mode": "subtitle",
      "languages": ["zh-CN", "en"],
      "commentary_profile": "betting",
      "deadline_ms": 8000
    }
  }'
FieldMeaning
mode"subtitle" for text or "audio" for synthesised audio. Required whenever output is supplied; missing it returns 400. Only omitting the entire output object uses service defaults.
languagesNon-empty list. Every language you intend to read must be listed here.
deadline_msOptional positive-integer time budget in milliseconds. Precedence: arm.buffer_ms, then output.deadline_ms, then the service-configured budget. The example explicitly requests 8000; it is not a universal default. See §10.
output.commentary_profileOptional: "betting" gives event-focused factual lines; "tv" adds objective play descriptions and relevant verified background. Neither promises continuous speech or complete event coverage. Precedence: output.commentary_profile, then match_package.commentary_profile, then the service setting (source default betting, which a deployment can override). Null or blank means omitted; surrounding whitespace and case are normalized. Other nonblank values return 400. GET /v1/matches/{match_id} returns output.effective_commentary_profile.

Response is 201 with the stored match, including the output you declared. Output settings are fixed at creation. The package, including its profile preference, may be updated only before arming; an explicit output profile still takes precedence.

4. Arm the match and get your SRT URL

POST/v1/matches/{match_id}/arm

{
  "match": { "status": "ARMED" },
  "session": { "stream_epoch": "epoch-7f3a...", "...": "..." },
  "ingest": {
    "protocol": "srt",
    "url": "srt://34.85.178.237:8890",
    "stream_id": "publish:live/<secret>:publisher:<secret>",
    "ffmpeg_url": "srt://34.85.178.237:8890?streamid=<issued>&pkt_size=1316"
  }
}

Arming reserves capacity and issues the publish credential. The match goes live when you actually start publishing, not when you arm it — deliberately two steps, so you can obtain the URL ahead of kickoff. Note the stream_epoch; a reconnect creates a new one.

If your publish attempt fails, do not re-arm. The session stays ARMED and the issued SRT URL remains valid — simply publish again. arm will refuse with “only a READY match can be armed” because the match is already armed, which is expected. Only after POST /v1/matches/{id}/end does the match reach a terminal state, and then you must create a new match_id.
An armed match holds capacity until it is ended. If you abandon an armed match without publishing, end it — otherwise it continues to occupy the active-match slot and later arm calls fail with active match capacity is already reserved.

5. Publish the video

ffmpeg -re -i input.ts \
  -map 0:v:0 -map 0:a? -c copy \
  -f mpegts \
  'srt://34.85.178.237:8890?streamid=<issued-stream-id>&pkt_size=1316'

Use the issued URL verbatim. Do not rewrite or log the streamid. Keep source PTS monotonic.

ItemContract
TransportSRT, caller/publisher mode, UDP 8890
ContainerMPEG-TS
Video codecH.264 recommended. H.265 is accepted by the ingest layer but has not been validated end to end — confirm before relying on it.
AudioOptional and ignored for subtitle purposes
TimestampsContinuous source PTS, milliseconds after normalisation
Ingest is IP-allowlisted. SRT contribution is restricted to the egress IP addresses you give us. If the handshake receives no response and times out, the first thing to check is whether the machine actually running ffmpeg egresses from the address we allowlisted — it is often not the same machine that ran your connectivity tests. Send us the correct address and we will add it.

Resolution matters; bitrate mostly does not

We decode your stream and analyse sampled frames at their source resolution — we do not downscale.

  • Bitrate affects only decode cost on our side. There is no cap and no recommendation.
  • Resolution directly drives per-frame processing cost and latency.

Recommendation: publish 1080p. 720p is fine. 4K buys nothing for commentary quality and multiplies cost and latency. The hard per-frame limit is 12 MB encoded, which 1080p never approaches.

6. Official match events — optional

POST/v1/events

If you have an official data feed, posting it materially improves accuracy: goals, cards and substitutions become confirmed facts rather than visual inferences. This requires a separate event token, issued on request.

Without an official feed, goals are still announced — but only once the broadcast itself confirms them. A false goal call is the one error a live broadcast cannot walk back, so the system waits for on-screen confirmation of a goal and never announces a replay as a new goal. An official feed makes goal, card and substitution calls faster and certain, and supplies the scorer’s name. If you have a feed, ask us for an event token.
{
  "match_id": "bundesliga-2026-001",
  "event_id": "provider-shot-88421",
  "revision": 1,
  "source_pts_ms": 4382000,
  "received_pts_ms": 4382600,
  "period": "2",
  "event_type": "SHOT",
  "team_id": "M05",
  "player_id": "M05:10",
  "player_name": "Amiri",
  "status": "confirmed",
  "authority": "official"
}

Retry the identical body to deduplicate; to correct a fact, keep event_id, increment revision, and set status to corrected or retracted. Events are accepted only while the match is live.

7. Read the subtitles

GET/v1/matches/{match_id}/subtitles

curl 'https://live.nextmoment.ai/v1/matches/bundesliga-2026-001/subtitles?after_sequence=0&language=en' \
  -H 'Authorization: Bearer <commentary-stream-token>'
{
  "match_id": "bundesliga-2026-001",
  "stream_epoch": "epoch-7f3a...",
  "language": "en",
  "subtitles": [
    {
      "schema_version": "eros.subtitle.v2",
      "sequence": 128,
      "line_id": "line-acde1234",
      "source_pts_ms": 51600,
      "priority": 1,
      "language": "en",
      "text": "Amiri cuts inside and shoots \u2014 saved.",
      "trace_id": "frame-52",
      "emitted_unix_ms": 1787068800123,
      "latency_ms": 5100,
      "audio_idempotency_key": null,
      "target_start_pts_ms": null,
      "duration_ms": null,
      "emotion": "Drama"
    }
  ]
}

language is required. There is no default — a request without it returns 400, so a missing parameter can never silently hand you a language you did not ask for. A language not listed in output.languages also returns 400.

after_sequence is an exclusive cursor: pass the highest sequence you have processed. It defaults to 0, meaning “from the beginning of what is retained”.

Polling before kickoff returns 200 with an empty list, not an error, as long as the match is armed. You can attach your poller ahead of the broadcast and leave it running.

WebSocket

wss://live.nextmoment.ai/v1/matches/{match_id}/subtitles/stream?after_sequence=<n>&language=en
Authorization: Bearer <commentary-stream-token>

The Authorization header is required during the handshake, so use a server-side WebSocket client that supports custom handshake headers. Each message is one JSON envelope, identical in shape to a polling entry. An unauthenticated connection is accepted at the HTTP layer and then closed immediately with code 1008; no data is sent.

8. Languages — how it works

Declare every language you want in output.languages at match creation. There is no way to add a language mid-match; asking for an undeclared language returns 400, so a typo cannot silently start a new language stream.

Availability can differ by language

Delivered versions of a line refer to the same source moment. They may become available at different times or be absent in a language; do not assume simultaneous arrival or complete coverage.

  • A line can occasionally be missing in one language when it cannot be produced in time for that language. It is then skipped in that language rather than delivered late.

Sequence numbers are shared across languages, and may have gaps

The same line carries the same sequence and the same line_id in every language, which makes cross-language reconciliation trivial: line 128 in English and line 128 in Chinese are the same moment of the match.

Do not treat a sequence gap as an error to retry. If you see 127 then 129 in English, line 128 was not produced in English. It is not packet loss, and retrying will not retrieve it. Advance your cursor and continue.

Player and team names

Supply authoritative spellings in match_package: name_zh, name_en, name_fr, name_es, name_pt-br and name_tr. The Portuguese output language is pt-BR, while its canonical package field is lowercase name_pt-br. Missing spellings may lead to another supplied spelling, a shirt number, generic wording, or an omitted language output; automatic naming is not guaranteed. Use short_name or the matching short_name_<lang> for spoken team names; keep the official name for recognition. aliases and tla help recognition; event team IDs should use the package team code.

9. Playing the lines — three rules

The stream is append-only. There is no revision, no cancellation, no acknowledgement to send back. A line you receive is final.

Rule 1 — Interruption is driven by priority

priorityContent
0Official match event
1Corroborated shot, save or goal
2Other on-ball action, and replays
3Tactical, evaluative, background
Lower numbers are more important. When a new line arrives whose priority is numerically lower than the line you are currently speaking, fade that line out and speak the new one. Otherwise, queue it. That single rule is why we send no cancellations: you already have everything needed to decide, and you can act on it at your own playback position rather than ours.

Rule 2 — Schedule against source_pts_ms

source_pts_ms is the moment in your media timeline that the line describes. Use it to align — not the HTTP arrival time.

Rule 3 — Lines can overlap in time

Two lines may describe overlapping moments. That is intentional; Rule 1 tells you what to do about it.

Voicing a line — emotion

emotion is the delivery mood of the line — exactly one of five fixed values, present on every line, never empty. It is set from the confirmed match situation the line reports, not from its wording. A replay is always Calm: it is never voiced as if it were happening again.

emotionWhen
CalmRoutine play, substitutions, kick-off, replays, background information
TensionDanger or a caution: a shot, a foul, a free kick, a corner, a yellow card
NervousThe outcome is still open: a VAR check, a penalty about to be taken, a chance developing
DramaA decisive turn that is not a goal: penalty awarded, red card, big save, goal ruled out
ExcitedA goal

If you synthesise the line yourself, map these values onto your voice’s style controls. Treat any value you do not recognise as Calm.

10. Latency — read this before you integrate

Use each line’s latency_ms to measure the service you are integrating; historical measurements are not a latency guarantee.

A line that misses its configured source-time deadline is dropped. The budget comes from arm.buffer_ms, then output.deadline_ms, then the service configuration. The source-code fallback is 30000 ms; confirm the actual deployment setting before integration.

Your text-to-speech time is not inside our budget. Our figures end at “text emitted”. Your audience latency is our latency + your synthesis + your playback scheduling. Measure your synthesis and scheduling time, then delay video playback enough to align it with the audio.

Every line carries latency_ms, so you can measure our half yourself rather than taking our word for it.

11. Errors

HTTPMeaningWhat to do
200Request acceptedContinue
400Missing or unconfigured language; malformed cursor; subtitle_callbackFix the request. Do not retry unchanged.
401Missing or wrong credentialReading subtitles needs the commentary stream token; the match token does not open that route.
404Unknown match, or the match has no session yetConfirm environment and match id; arm the match before reading.
409State conflict — e.g. arming an already-armed match, or ending a match with no active sessionRefresh state before retrying. See §4.
429 / 5xxTemporaryRetry the identical request with capped exponential backoff and jitter.

All *_pts_ms values are milliseconds on your source media timeline, not wall-clock. emitted_unix_ms is wall-clock.

12. Integration checklist

Store both tokens server-side.
Confirm your subtitle consumer uses the commentary stream token, not the match token.
Create a match
With output.mode = "subtitle" and every language you need.
Arm it; store the stream_epoch.
Confirm the machine that will publish egresses from an allowlisted IP.
Publish 1080p H.264 over SRT
Using the issued URL verbatim.
Poll per language
Advancing the cursor by the highest sequence you processed.
Implement the priority interruption rule
And tolerate sequence gaps without retrying.
Delay your video playback
To absorb your own synthesis time.
Brief your QA team on §6
No goal announcements without an official feed is expected behaviour.

13. Changelog

This page is the current version. Re-fetch this URL rather than working from a downloaded copy.

DateChange
2026-09-25Goals are announced without an official feed once the broadcast confirms them (§6). Languages section rewritten (§8).
2026-09-24Every line now carries emotion (§9). The field is additive; the envelope stays eros.subtitle.v2. If you validate payloads strictly against a schema copy taken before this date, refresh it: the schemas do not allow unknown keys.
2026-09-05Published at a stable public URL. Added: recovery flow after a failed publish (§4), armed-match capacity behaviour (§4), SRT IP allowlisting (§5), pre-kickoff polling returns 200 not 409 (§7, §11), goal-announcement gate stated explicitly (§6).
2026-09-04Subtitle delivery, first release.