Skip to content

Guide

Handles time-based programming for live channels. No external services required — the scheduling domain only talks to PostgreSQL.


Running

  1. Clone all required repositories and install — follow Installation
  2. cd Quickstart
  3. make playout-d
  4. Test: curl localhost:3000/api/v1/scheduling/epg

The scheduling domain starts automatically with CoreAPI — no separate service.


Required services

Scheduling only depends on PostgreSQL. No SeaweedFS, tusd, or NATS required for basic EPG functionality.

Service Role
postgres Stores schedule_slots table — AutoMigrate creates it on CoreAPI boot

NATS is optional and currently a no-op (SCHEDULING_NATS_ENABLED=false by default). When enabled in a future iteration it will publish slot activation events.


Creating your first schedule

Step 1 — confirm you have at least one channel:

curl http://localhost:3000/api/v1/playback/channels

If empty, create a channel first via PlayoutAdmin or the playback API. The EPG grid returns a hint when no channels exist.

Step 2 — create a program slot:

curl -X POST http://localhost:3000/api/v1/scheduling/slots \
  -H "Content-Type: application/json" \
  -d '{
    "channelId": 1,
    "title": "Morning News",
    "startTime": "2026-07-03T01:00:00Z",
    "endTime": "2026-07-03T03:00:00Z",
    "genre": "News",
    "description": "Daily morning news bulletin"
  }'

All times are RFC3339 UTC. The frontend converts to local timezone on display.

Step 3 — verify the EPG grid:

curl "http://localhost:3000/api/v1/scheduling/epg?date=2026-07-03"

Returns all channels with their slots for that day. The channels array is ordered by sid — channel number order.


Key behaviours

Overlap validation — two slots on the same channel cannot overlap. The API returns 409 Conflict if a new slot would overlap an existing one. Touching (end of slot A = start of slot B) is allowed — the overlap check is start_time < new_end AND end_time > new_start, so adjacent slots are fine.

Empty channels — channels with no slots for a given day are included in the grid with an empty slots: []. The UI renders them as blank rows.

No channels — if the channels table is empty, the grid returns { "channels": [], "hint": "no channels found — create channels first" } rather than an error.

Now/nextGET /epg/:channelId/now returns the currently airing program (now) and the next scheduled program (next). Both can be null — now is null if nothing is scheduled for the current UTC time, next is null if no future programs are scheduled.

UTC everywhere — all times stored and returned as UTC. The frontend is responsible for converting to the viewer's local timezone (e.g. Bangkok UTC+7 for Thai viewers, other timezones for overseas deployments).


Timezone note for the UI

// slot.startTime arrives as UTC ISO string: "2026-07-03T01:00:00Z"
// convert to Bangkok time for display:
const bkk = new Date(slot.startTime).toLocaleTimeString("th-TH", {
  timeZone: "Asia/Bangkok",
  hour: "2-digit",
  minute: "2-digit",
})
// → "08:00" (UTC+7)

For a multi-region product, pass the viewer's timezone from their browser:

const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
// → "Asia/Bangkok" | "Europe/London" | "America/New_York" etc.

EPG grid UI data flow

The grid (channels as rows, time as columns) is driven by a single API call per date:

GET /api/v1/scheduling/epg?date=YYYY-MM-DD

The frontend: 1. Renders one row per channel in the channels array 2. For each slot, calculates its pixel width from (endTime - startTime) and its left offset from startTime - dayStart 3. Converts all times to local timezone before rendering 4. Polls GET /epg/:channelId/now every few minutes to update the "now playing" overlay


Planned features

BlackoutRule — suppress a slot for specific regions or subscriber tiers. Common for sports rights (e.g. "don't show Premier League in UK for free tier"). Requires client to send context (location, subscription level) with EPG requests.

RecurrenceRule — iCal-style RRULE patterns. "Every Monday at 20:00, 2 hours, title=Premier League." A background job or on-demand expander generates ScheduleSlot instances for the next N days.

NATS slot activation — when SCHEDULING_NATS_ENABLED=true, the domain will publish a slot.activated event to NATS when a slot's startTime is reached. Other domains (playback, analytics, notifications) subscribe independently — scheduling doesn't need to know who's listening.

XMLTV import — bulk schedule import via POST /scheduling/epg/import accepting standard XMLTV XML. Useful for loading broadcaster-supplied EPG data.