Revue
English

Developer Docs · Markdown

Revue Server Protocol

Version 4 · Status: stable

Revue is a live TV player for Apple TV. Besides M3U playlists, it can show live rooms from your own server. This document describes the HTTP interface such a server provides. If you build a server that follows it, Revue lists its rooms, shows which ones are live, plays them, and turns on optional features such as live comments.

Revue does not relay your streams and has no account system. The server address is stored only on the Apple TV, and Revue connects to it directly.

For AI agents: this page is the complete specification. Implement the required parts in §2–§5, then go through the checklist in §9 before you finish.

1. Overview

A Revue server is a normal HTTP server on the user’s local network. One server can offer several sources. Each source is a list of sections, and each section is a list of rooms. A room is one live stream.

Revue ──► GET /api/sources                  which sources does this server offer?
      ──► GET {source.url}/api/sections     rooms of one source, grouped into sections
      ──► GET {room.streamUrl}              play: an HLS master playlist, or 410 when not live
      ──► GET {source.url}/api/rooms/{id}   refresh one saved room
      ──► {room.actions.*}                  optional capabilities, declared per room

Design rules:

  • The client renders what the server declares. Revue knows nothing about where your streams come from. Which rooms exist, which are live, which features they support, and how their status is labeled are all decided by the server.
  • The server provides every URL. Apart from the two paths in §4.2 and §4.4, Revue only uses URLs it received from the server. It never builds a URL from an identifier.
  • A URL in actions turns a feature on. If the URL is absent, the feature is off and Revue hides it.

2. Discovery

Revue finds a server in one of two ways. Both lead to the same server root URL, http://<host>:<port>.

Manual entry. The user types the root URL in Revue’s settings. Both http:// and https:// work (lowercase); without a scheme, Revue assumes http://.

Bonjour (mDNS). Advertise exactly one record per server process:

Item Value
Service type _livehub._tcp (a fixed technical constant; it cannot change)
Instance name Shown to the user. Keep it the same across restarts: Revue uses it to find a saved server again after its IP address changes
TXT ver "4". Revue does not use a record with any other value, and marks it as incompatible in its settings
Host / port Your server. Revue resolves an IPv4 address

The number of sources does not change the number of records. Sources are listed by /api/sources.

3. Conventions

  • JSON. Responses are UTF-8 JSON unless stated otherwise.
  • Absolute URLs. Every URL in a response is absolute (http://192.168.1.20:8800/...). Build them from the Host header of the request, so they work with whatever address the client used. A fixed address that you configure is fine too, as long as Revue can reach it.
  • No caching. Every JSON endpoint in this document responds with Cache-Control: no-store. Revue requests the room list with default HTTP caching, so without this header it can keep showing an old list.
  • Unknown fields are ignored. You may add your own fields to any object. Revue ignores fields it does not know, with the reserved names below.
  • Reserved names. Do not send a room field named flvUrl, and do not use paths under /api/cast/. Revue uses them for features outside this document. Answer any path you do not implement with 404.
  • Exact types. Revue decodes each response as a whole. One field with the wrong type in one room, such as viewers sent as "1280" or 1280.0, makes the whole list fail to load. Integers must be JSON integers.
  • Errors. A successful response has "ok": true. A failed request returns a non-2xx status, with the body {"ok": false, "error": "<human-readable text>"}. The 410 in §4.5 is the one exception and has its own body. Do not send "ok": false with status 200. You may add errorCode (a string) for diagnostics. Revue does not show error to the user and does not branch on error or errorCode, except for "errorCode": "NOT_LIVE" on a 410 (§4.5).
  • Status codes carry meaning. Use them precisely:
Situation Status
Success 200
The room exists but is not live (stream endpoints only, §4.5) 410
The thing is confirmed not to exist 404
The upstream failed, timed out, or you cannot tell right now 5xx (usually 502 or 503)

“Cannot tell” is never “not found” and never “not live”. Revue keeps its current state on 5xx and tries again later. A wrong 404 marks a saved room as unavailable; a wrong 410 stops playback.

  • Request headers. Revue sends two standard headers on its requests to your server:

    • Accept-Language: the app’s current interface language, as one tag such as en, zh-Hans, or zh-Hant. It changes as soon as the user changes the language in Revue. You may use it to localize statusLabel.
    • User-Agent: Revue/<version> (<build>; tvOS <system version>), for example Revue/1.1.2 (607; tvOS 27.0). Use it for logs only. Do not change behavior based on it. Compatibility is decided by apiVersion (§7).

    The system video player requests streamUrl, its playlists, and its segments with its own headers. Before Revue shows the offline page, it requests streamUrl itself with the headers above, so a localized statusLabel in the 410 body reaches the user.

4. Endpoints

4.1 GET /api/sources

Relative to the server root. Lists the sources of this server. This is the first request Revue makes.

{
  "apiVersion": 4,
  "ok": true,
  "sources": [
    { "id": "outdoors", "name": "Outdoors", "url": "http://192.168.1.20:8800/outdoors" }
  ]
}
Field Type Required Meaning
apiVersion integer, always 4 yes With any other value, Revue cannot use the server and shows that it received no source list
ok boolean yes
sources array, at least one item yes Display order
sources[].id string yes Stable identifier. Revue uses it to remember the selected source and to store saved rooms, so never change it
sources[].name string yes Tab label
sources[].url string (URL) yes Base URL of the source, absolute, no trailing slash. Revue treats it as opaque and resolves the source endpoints against it

Keep this endpoint cheap. Revue calls it on launch and whenever the server address changes.

4.2 GET {source.url}/api/sections

The room list of one source. Revue appends this path to source.url. Both query parameters are optional:

  • refresh=1 asks you to bypass your own cache. Revue sends it when the user refreshes by hand.
  • maxAge=<seconds> says how old the data may be (age = now − generatedAt). Supporting it is optional. If your data is fresh enough, answer at once. If not, start a refresh, or join the one already running, and answer when it finishes. Do not hold the request longer than about 10 seconds: after that, answer with the data you have and "refreshing": true, and Revue asks again right away. If you ignore maxAge, Revue re-fetches on its own schedule instead.
{
  "apiVersion": 4,
  "ok": true,
  "generatedAt": "2026-10-24T12:00:00.000Z",
  "sections": [
    {
      "id": "featured",
      "title": "Featured",
      "rooms": [
        {
          "id": "sunrise",
          "slug": "1024",
          "name": "Savanna sunrise flight",
          "group": "Kito Mwangi",
          "live": true,
          "viewers": 1280,
          "coverImageUrl": "http://192.168.1.20:8800/covers/sunrise.jpg?v=7",
          "streamUrl": "http://192.168.1.20:8800/outdoors/hls/sunrise.m3u8",
          "actions": {
            "statusUrl": "http://192.168.1.20:8800/outdoors/api/rooms/sunrise/status"
          }
        }
      ]
    }
  ]
}
Field Type Required Meaning
apiVersion integer, always 4 yes
ok boolean yes
generatedAt string yes When this data was actually produced, in UTC: 2026-10-24T12:00:00Z or 2026-10-24T12:00:00.000Z. When you serve cached data after a failed refresh, keep the time of the last success
stale boolean no (default false) true when this is cached data because the latest refresh failed. Revue tells the user when they refresh by hand
refreshing boolean no (default false) Only for requests with maxAge: true when your data is older than asked and your refresh is still running. Revue asks again at once
playbackHints object no See §6
sections array yes The final display list. Filter and sort on the server. Do not send empty sections (Revue hides them)
sections[].id string yes Unique within the source and stable across refreshes
sections[].title string yes Section heading
sections[].rooms array of rooms yes See §4.3

4.3 Room object

Field Type Required Meaning
id string yes The room’s identity. See “Identity” below
name string yes Room title
slug string no A short human-readable handle, such as a channel number or a username. Display only: Revue shows it next to the title in the player when it differs from name
group string no Secondary line on the room card, such as the host’s name. Revue shows slug when absent
live boolean yes true when streamUrl would play live video right now. Revue uses it for the LIVE badge. It is a hint: the authoritative “not live” signal is a 410 from streamUrl (§4.5)
status string no Opaque value for your own debugging. Revue never branches on it. The 410 body in §4.5 is the one exception
statusLabel string no Human-readable status, such as “Replay”. Shown on the offline page and for saved rooms. Localize it on the server
viewers integer ≥ 0 no Viewer count. Omit it when you do not know; do not send 0 for “unknown”
isFollowing boolean no (default false) Only meaningful with the follow capability (§5.2)
coverImageUrl string (URL) no Cover image. Revue shows a plain placeholder when absent. See “Cover images” below
streamUrl string (URL) yes The stream endpoint, §4.5
actions object yes (may be {}) Optional capabilities, §5

Identity. id identifies the room, not its position in a list:

  • It is unique within the source, never changes for the same room, and contains no /.
  • When the same room appears in several sections, it has the same id in each of them.
  • Revue uses it to recognize the room across refreshes, to remove duplicates when the user switches rooms with the remote, to store saved rooms, and to build the path in §4.4. Saved rooms are stored by source id plus room id, so changing either one loses the user’s saved rooms.
  • slug is never used for identity. You may change it at any time.

Live status. live must be true to the source. Do not mark a room offline only because it dropped out of a list you fetched; look it up. If you cannot find out, keep the last known value or fail with 5xx.

Cover images. Revue caches each image by URL for as long as the app runs, and does not retry a URL that failed once. When the picture changes, change the URL, for example with a version query such as ?v=7.

4.4 GET {source.url}/api/rooms/{id}

One room, looked up by its id. Revue percent-encodes id as one path segment. Revue calls this endpoint to refresh each saved room when the user opens the saved rooms page. Only a room that was refreshed successfully can be played from the saved list, so this endpoint is required.

{
  "apiVersion": 4,
  "ok": true,
  "generatedAt": "2026-10-24T12:00:00.000Z",
  "room": { "id": "sunrise", "name": "Savanna sunrise flight", "live": false, "streamUrl": "...", "actions": {} }
}
  • room has the same shape as a room in §4.3, including streamUrl and actions.
  • Return the room even when it is not live ("live": false). Do not filter it out.
  • Return 404 only when the room is confirmed not to exist. A saved room that is still live must not disappear because it is not in your current list: look it up at its origin.
  • Upstream failure is 5xx. Revue then keeps showing its last snapshot.
  • Methods other than GET return 405.

4.5 GET {room.streamUrl}

  • Live: 200 with an HLS master playlist (application/vnd.apple.mpegurl). Anything AVPlayer can play works, including HEVC and HDR.

  • Not live: 410 Gone with Cache-Control: no-store and this body:

    { "ok": false, "errorCode": "NOT_LIVE", "statusLabel": "Offline", "status": "offline" }
    

    In this body, status has defined values, unlike the room object’s status (§4.3). Revue uses it only to pick the icon on the offline page:

    status Meaning
    offline The broadcaster is not live
    private The room is live, but the broadcast is not public (for example, members only)
    hidden The broadcaster has turned the video off for now
    away The broadcaster has stepped away for now. The room is still open
    replay The broadcaster is not live, and the room is showing a recording
    unavailable Any other reason, or you cannot tell which

    Revue treats a value it does not know as unavailable. New values may be added later. Existing values keep their meaning.

    "errorCode": "NOT_LIVE" is required: a 410 without it is treated as a playback failure. With it, Revue shows an offline page instead of an error. Send 410 only when you have confirmed the room is not live. If a child playlist is requested after the stream ends, answer it with 410 in the same way.

  • Confirmed not to exist: 404. Cannot tell right now: 5xx.

You may answer a live streamUrl with a 302 redirect to an HLS master playlist on another host. The 410 must still come from your server.

Repeated requests. Revue requests streamUrl more than once, and not only from the player: before playing a room marked not live, to read the stream’s format, while recovering from a stall, and on the pages below. Every GET must work and must not end the session that an earlier request started. Answer within a few seconds: these checks time out after 8 seconds and then count as “cannot tell”.

Offline and error pages. While the offline page is shown, Revue requests streamUrl every statusPollSeconds (§6): a new 410 updates the page, and a 200 resumes playback. If a room drops back to the offline page within 60 seconds of resuming, Revue waits until those 60 seconds have passed before resuming again. While a playback error is shown, Revue also requests streamUrl every statusPollSeconds: a 410 switches to the offline page, and a 200 retries playback once the error has been shown for 60 seconds.

Revue never adds query parameters to streamUrl. Put everything you need into the URL itself.

5. Capabilities (actions)

Every member is optional and holds an absolute URL. A present URL turns the capability on. Pairs must be complete; a half pair is treated as off.

Member Method Capability
statusUrl GET Room status polling, §5.1
followUrl + unfollowUrl POST Follow and unfollow, §5.2
streamOptionsUrl + switchStreamUrl GET / POST Pick a line or a quality, §5.3
switchEdgeUrl GET Ask the server to pick another upstream, §5.4
danmakuUrl GET Live comments, §5.5

5.1 GET {statusUrl}

Revue polls it every statusPollSeconds while a room plays.

{ "ok": true, "live": true, "statusLabel": "Live", "bitrateKbps": 6200 }
Field Type Required Meaning
ok boolean yes
live boolean yes (when ok) Same meaning as the room’s live. A response without it is ignored as a whole
statusLabel string no Current status text
bitrateKbps integer no The recent total bitrate (audio + video) of this room as seen by your server. Revue shows it in the player. Omit it when you do not know
  • Unknown room: 404. Upstream failure: 5xx. Revue keeps its state and keeps polling in both cases; it never reloads playback because of a failed poll.
  • live must be real, looked up the same way as in §4.4.
  • Keep it cheap: every viewer calls it every few seconds.

5.2 Follow

POST {followUrl} and POST {unfollowUrl} have no body. Respond with:

{ "ok": true, "following": true }

following is the state after the operation. Any non-2xx status is a failure.

5.3 Lines and qualities

For rooms that come in several variants, such as different qualities, or the same content from different origins (“lines”).

GET {streamOptionsUrl}:

{
  "ok": true,
  "lines": [ { "id": "a", "label": "Line A" }, { "id": "b", "label": "Line B" } ],
  "qualities": [ { "id": "1080", "label": "1080p" }, { "id": "720", "label": "720p" } ],
  "currentLineId": "a",
  "currentQualityId": "1080"
}
  • lines and qualities are required arrays. Send [] when you have none to offer.
  • A line may also have provider (string). Revue groups lines with the same provider under that heading.
  • currentLineId and currentQualityId are optional. Omit them when you cannot tell; do not send an empty string.
  • Ids are opaque strings. Revue only shows and returns them.

POST {switchStreamUrl} with a JSON body {"lineId": "b"} or {"qualityId": "720"}. Accept both together as well. Respond 200 {"ok": true}. An unknown id or a wrong body is 400 {"ok": false, "error": "...", "errorCode": "INVALID_INPUT"}.

Remember the choice per room on the server, and apply it to streamUrl. After a successful switch, Revue reloads streamUrl.

5.4 GET {switchEdgeUrl}

Asks the server to pick a different upstream for this room. Revue calls it during automatic recovery (at most twice while the same room keeps playing), when the user retries from the error page, and when the user refreshes a room without §5.3. Revue ignores the response and then reloads streamUrl. When absent, Revue simply reloads. If the user picked a quality with §5.3, keep it.

5.5 Live comments: GET {danmakuUrl}

A long-lived response with Content-Type: application/x-ndjson, sent with chunked transfer encoding and Cache-Control: no-store. Each line is one JSON object followed by \n. Keep the connection open until the client disconnects.

type Fields Meaning
chat text (required), color (optional, 24-bit RGB integer such as 16777215; white when absent), nick (optional, not displayed), ts (optional, milliseconds) One comment. Revue displays text
hb none Heartbeat. Send one immediately after the connection opens, then every 15 seconds
error reason Why the server refuses or ends the stream, such as busy or shutdown. For your logs; Revue does not display it
{"type":"hb"}
{"type":"chat","text":"hello","color":16777215,"ts":1790000000000}
{"type":"hb"}
  • Revue ignores unknown type values.
  • Revue reconnects when the stream ends, fails, or sends nothing for 45 seconds. It backs off from 1 second up to 30 seconds between attempts. Any line received resets the back-off to 1 second, so do not accept a connection and then close it right away: Revue would reconnect about every second.
  • There is no replay: comments sent while disconnected are lost.
  • A room that is not live may send only heartbeats. That is not an error.
  • You may drop or shorten comments to protect your server. Revue does not treat gaps as errors.
  • To refuse (for example, too many connections), respond 503 with a single line {"type":"error","reason":"busy"}.

Comments come only from your server. Revue shows them only when the user turns them on.

6. Playback hints

playbackHints in §4.2 tunes playback for the rooms of that source. All fields are optional and in seconds. Revue clamps every value to the range shown.

Field Default Range Meaning
statusPollSeconds 10 5–60 Interval of statusUrl polling, and of streamUrl checks on the offline and error pages
startupTimeoutSeconds 15 5–60 Recover if playback has not started in this time
stallRecoverySeconds 25 10–120 Recover if playback has made no progress in this time
liveHoldBackSeconds 8 4–30 When playback falls more than 30 seconds behind the live edge, Revue jumps forward to this distance behind it. The starting position comes from the HLS playlist, as in any AVPlayer stream

Send hints only when your streams need them. The defaults suit most HLS streams.

7. Versioning

  • Two checks: the Bonjour TXT ver must be "4", and every apiVersion must be 4.
  • New optional fields and new optional endpoints do not change the version. Revue ignores what it does not know.
  • Changing a required field or the meaning of an existing field changes the version.

8. JSON Schemas

Machine-readable response shapes (JSON Schema, draft 2020-12):

  • https://revue.vastlogic.dev/docs/server-protocol/schema/v4/sources-response.schema.json
  • https://revue.vastlogic.dev/docs/server-protocol/schema/v4/sections-response.schema.json
  • https://revue.vastlogic.dev/docs/server-protocol/schema/v4/room-response.schema.json
  • https://revue.vastlogic.dev/docs/server-protocol/schema/v4/room-status-response.schema.json
  • https://revue.vastlogic.dev/docs/server-protocol/schema/v4/follow-response.schema.json
  • https://revue.vastlogic.dev/docs/server-protocol/schema/v4/stream-options-response.schema.json
  • https://revue.vastlogic.dev/docs/server-protocol/schema/v4/danmaku-message.schema.json

The schemas cannot check uniqueness, stable identity, or the meaning of status codes. The checklist below covers those.

9. Implementation checklist

Most broken servers run fine but behave wrongly. Check each item before you call the server done.

Discovery and basics

  • GET /api/sources works at the server root and returns at least one source.
  • Bonjour advertises _livehub._tcp with TXT ver=4, one record per process, with an instance name that stays the same across restarts (or the user is told to enter the address manually).
  • Every URL in every response is absolute and reachable from the Apple TV. No localhost or 127.0.0.1 in responses.
  • Every JSON endpoint sends Cache-Control: no-store.
  • apiVersion is the integer 4 in sources, sections and room responses.
  • Paths you do not implement return 404. No room field named flvUrl, no paths under /api/cast/.
  • Every field has exactly the type in this document. Integers are JSON integers.

Lists and rooms

  • generatedAt is when the data was produced, not when the response was sent. It moves forward when you really refresh, and stays put when you serve cached data. (Revue re-fetches a list that looks old; a frozen generatedAt without stale makes it re-fetch for about two minutes.)
  • generatedAt is in UTC and ends with Z, optionally with milliseconds.
  • No empty sections. Section ids are unique within the source.
  • Room id is unique within the source, contains no /, never changes, and is the same in every section the room appears in.
  • actions is present on every room, even if it is {}.
  • viewers is omitted, not 0, when you do not know it.
  • A cover image that changes gets a new URL.
  • GET {source.url}/api/rooms/{id} is implemented, returns offline rooms with "live": false, and finds rooms that are not in the current list.

Live status and playback

  • live comes from a real lookup. A room that dropped out of a list is not offline until you have checked.
  • streamUrl returns 410 with "errorCode": "NOT_LIVE" in the body, and only when the room is confirmed not live.
  • streamUrl can be requested again at any time without breaking playback that is already running.
  • streamUrl answers within a few seconds, whether it serves the playlist or redirects.
  • 404 only when the room is confirmed not to exist.
  • Every upstream failure or timeout is 5xx, never 404 or 410.

Capabilities

  • followUrl and unfollowUrl appear together or not at all. Same for streamOptionsUrl and switchStreamUrl.
  • currentLineId and currentQualityId are omitted, not empty, when unknown.
  • The comment stream sends a heartbeat immediately and then every 15 seconds, one JSON object per line, and does not close a connection right after accepting it.
  • statusUrl is cheap enough to be called every few seconds by every viewer.

Before you finish

  • Validate each response against the schemas in §8.
  • Add the server in Revue, play a live room, and open an offline room to see the offline page.