# HLS Playlist Generator Design
## Linear Scheduled Playback Engine

## Strategy

Videos are encoded once into HLS segments and stored on disk:

```
/live/hls/PLAYLIST/videoId/
    index.m3u8       per-video VOD playlist
    segments.json    segment metadata (durations, URIs)
    seg00000.ts
    seg00001.ts
    ...
```

`stream.m3u8` is a rolling live playlist that **references those pre-encoded segments directly** — no FFmpeg re-encoding at runtime.

```
/live/stream.m3u8:
    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:7
    #EXT-X-MEDIA-SEQUENCE:...

    #EXTINF:6.006,
    hls/Esther/vid001/seg00042.ts
    #EXTINF:5.972,
    hls/Esther/vid001/seg00043.ts
    #EXT-X-DISCONTINUITY
    #EXTINF:6.0,
    hls/sunday/mqcqALOjcQ8/seg00000.ts
    ...
```

`start_live_stream.sh` rewrites `stream.m3u8` every 5 seconds by reading
`schedule.json` and the precomputed `segments.json` files.

---

## 1. Purpose

Turn encoded video assets and a daily schedule into one continuous Roku playback experience via the stable public URL:

```
https://media.abcc.org/live/stream.m3u8
```

The URL never changes. The playlist content shifts with the schedule.

---

## 2. Inputs

### 2.1 Pre-encoded HLS segments

Each MP4 is encoded once by `encode_videos.sh` (stream-copy, no re-encode):

```
live/hls/{playlist}/{videoId}/
    seg00000.ts … seg0NNNN.ts
    index.m3u8          VOD playlist for this video
    segments.json       machine-readable segment list
```

`segments.json` format:

```json
{
  "videoId": "vid001",
  "playlist": "Esther",
  "durationSec": 3120,
  "hlsPath": "/live/hls/Esther/vid001/index.m3u8",
  "segments": [
    { "index": 0, "uri": "/live/hls/Esther/vid001/seg00000.ts", "duration": 6.006 },
    { "index": 1, "uri": "/live/hls/Esther/vid001/seg00001.ts", "duration": 5.972 }
  ]
}
```

### 2.2 Daily Schedule (`data/schedule.json`)

Two entry types:

| `start` value | Meaning |
|---|---|
| `"HH:MM"` | Scheduled — starts at that wall-clock time |
| `"after"` | Filler — plays after the active block's content runs out |

Three media reference types:

| `type` | Resolves to |
|---|---|
| `"recent-sunday"` | Latest video in `videos/sunday/list.txt` |
| `"recent-youth"` | Latest video in `videos/youth/list.txt` |
| `"video"` | Single video by `id` |
| `"playlist"` | All videos in `videos/{id}/list.txt`, ordered by `mode` |

Playlist modes: `series` (file order), `series-repeat` (loop), `random` (date-seeded shuffle — same order all day for all clients).

Example schedule block:

```json
{ "start": "10:00", "media": { "type": "recent-sunday" } }
{ "start": "after", "media": { "type": "playlist", "id": "BT-new-Hymns", "mode": "random" } }
```

---

## 3. Output

A rolling HLS media playlist (`stream.m3u8`) covering the next ~60 seconds (10 × 6-second segments):

```m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:7
#EXT-X-MEDIA-SEQUENCE:12500

#EXTINF:6.006,
hls/Esther/vid001/seg00042.ts
#EXTINF:5.972,
hls/Esther/vid001/seg00043.ts
#EXT-X-DISCONTINUITY
#EXTINF:6.0,
hls/sunday/mqcqALOjcQ8/seg00000.ts
```

Segment paths are relative to `live/` (where `stream.m3u8` lives).

---

## 4. Core Resolution Logic

### Step 1: Determine Active Block

Priority order for today:
1. `dates["YYYY-MM-DD"]` — date-specific override
2. `defaults.Sunday` — if today is Sunday and no date entry
3. `defaults.every-day` — fallback

Select the latest scheduled block whose `start` ≤ current time (ignoring `"after"` entries).

### Step 2: Compute Elapsed Time

```
elapsed = now_seconds_since_midnight - block_start_seconds
```

### Step 3: Resolve Video List

Collect videos from:
1. Active block's `media` object
2. All `"after"` filler blocks (appended in order)

### Step 4: Walk to Current Position

Walk the video list, subtracting each video's `durationSec` from `elapsed` until the current video is found. Then walk that video's individual segment durations to find the exact segment index.

### Step 5: Build Window

Emit `WINDOW` (default: 10) segments starting from the current index. Insert `#EXT-X-DISCONTINUITY` when crossing from one video to the next.

### Step 6: Atomic Write

Write to a temp file, then `mv` into place — no partial reads by clients.

---

## 5. Playlist Window

| Property | Value |
|---|---|
| Segment duration | ~6 sec |
| Window size | 10 segments |
| Lookahead | ~60 sec |
| Refresh interval | 5 sec |

---

## 6. Edge Cases

| Situation | Behavior |
|---|---|
| Block overrun (all videos aired) | Restart from first video in block |
| Video not found in `hls/` | Skip, continue |
| `segments.json` missing | Skip video |
| No active block | Skip write, retry in 5s |

---

## 7. File Locations

| Path | Role |
|---|---|
| `live/stream.m3u8` | Live rolling playlist (written every 5s) |
| `live/hls/{playlist}/{videoId}/` | Pre-encoded segments |
| `live/hls/{playlist}/{videoId}/segments.json` | Segment metadata |
| `live/scripts/start_live_stream.sh` | Playlist generator (runs as systemd service) |
| `live/scripts/encode_videos.sh` | One-time encoder (run via cron or manually) |
| `data/schedule.json` | Daily schedule definition |

---

## 8. Discontinuities

`#EXT-X-DISCONTINUITY` is inserted whenever playback crosses from one source video into the next. This lets the player handle codec/timestamp boundary transitions cleanly.

---

## 9. Caching

| Asset | Cache Policy |
|---|---|
| `stream.m3u8` | `no-cache` or very short TTL |
| per-video `index.m3u8` | short TTL |
| `.ts` segments | long TTL (immutable) |

---

## 10. Time-Boundary Test Cases

```
07:59:59 → previous block
08:00:00 → new block starts
11:59:59 → last second of current block
12:00:00 → next block
23:59:59 → last second of day
00:00:00 → midnight rollover
```

Also test: missing video, missing segments.json, filler-only schedule, block overrun.
