Guide
Handles time-based programming for live channels. No external services required — the scheduling domain only talks to PostgreSQL.
Running
- Clone all required repositories and install — follow Installation
cd Quickstartmake playout-d- 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/next — GET /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.