Below is the **third technical document** focused on the *HLS Playlist Generator**.
*
Suggested file name:

```text
docs/hls-playlist-generator-design.md
```

---

# HLS Playlist Generator Design

## Roku Church Channel – Linear Scheduled Playback Engine

---

# 1. Purpose

This document describes the design of the **HLS Playlist Generator** used by the church Roku channel.

The generator is responsible for producing the continuous playback URL:

```text
https://media.yourchurch.org/hls/channel.m3u8
```

This URL remains constant, while the playlist content changes according to the current schedule.

The generator makes the Roku channel behave like a **24/7 television channel** built from pre-recorded content.

---

# 2. Problem Statement

The church channel is not live streaming.

Instead, it uses:

* pre-recorded videos
* 4-hour programming packages
* a daily schedule
* one continuous public HLS URL

The system must answer this question at all times:

**“What should be playing right now?”**

And then return the correct HLS playlist to Roku.

---

# 3. Core Design Principle

The Roku client should never manage scheduling.

The Roku client only plays:

```text
https://media.yourchurch.org/hls/channel.m3u8
```

The backend generator is responsible for:

* determining the active 4-hour block
* loading the assigned package
* determining the correct video and segment offset
* generating the current HLS playlist window

---

# 4. Inputs

The playlist generator depends on four data sources.

## 4.1 Video Catalog

Each encoded video has metadata.

Example:

```json
{
  "videoId": "sermon-2026-03-01",
  "title": "The Grace of God",
  "durationSec": 3120,
  "hlsMasterPath": "/hls/sermon-2026-03-01/master.m3u8",
  "defaultVariantPath": "/hls/sermon-2026-03-01/720p/index.m3u8"
}
```

## 4.2 Package Definition

A package is an ordered list of videos.

Example:

```json
{
  "packageId": "PKG-SUNDAY-CURRENT",
  "durationHours": 4,
  "items": [
    { "videoId": "worship-021" },
    { "videoId": "sermon-2026-03-01" },
    { "videoId": "announcements-005" },
    { "videoId": "teaching-018" }
  ]
}
```

## 4.3 Daily Schedule

Each day has six 4-hour blocks.

Example:

```json
{
  "date": "2026-03-08",
  "blocks": [
    { "start": "00:00", "packageId": "PKG-NIGHT-01" },
    { "start": "04:00", "packageId": "PKG-MORNING-01" },
    { "start": "08:00", "packageId": "PKG-SUNDAY-CURRENT" },
    { "start": "12:00", "packageId": "PKG-SUNDAY-CURRENT" },
    { "start": "16:00", "packageId": "PKG-EVENING-01" },
    { "start": "20:00", "packageId": "PKG-SUNDAY-LAST" }
  ]
}
```

## 4.4 Encoded HLS Assets

Each video is encoded once into HLS.

Example:

```text
/hls/sermon-2026-03-01/720p/index.m3u8
/hls/sermon-2026-03-01/720p/seg0001.ts
/hls/sermon-2026-03-01/720p/seg0002.ts
```

---

# 5. Output

The generator returns an HLS media playlist.

Example:

```m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:12500

#EXTINF:6.0,
/hls/worship-021/720p/seg0041.ts

#EXTINF:6.0,
/hls/worship-021/720p/seg0042.ts

#EXT-X-DISCONTINUITY
#EXTINF:6.0,
/hls/sermon-2026-03-01/720p/seg0001.ts
```

This playlist represents the **next window of playback** for the current channel state.

---

# 6. Generator Responsibilities

The generator must:

* determine current time
* find active schedule block
* load package for that block
* calculate elapsed time inside that package block
* determine which video item is active
* determine which HLS segments should be returned
* include discontinuity markers when switching videos
* return a valid HLS playlist

---

# 7. Scheduling Logic

## 7.1 Block Selection

Given current local time:

```text
2026-03-08 09:17:25
```

The generator determines the active block:

```text
08:00–12:00 → PKG-SUNDAY-CURRENT
```

## 7.2 Elapsed Time in Block

Block start:

```text
08:00:00
```

Current time:

```text
09:17:25
```

Elapsed time in block:

```text
1 hour 17 minutes 25 seconds
= 4645 seconds
```

## 7.3 Package Offset Resolution

The generator walks through package items in order:

* worship-021 → 900 sec
* sermon-2026-03-01 → 3120 sec
* announcements-005 → 300 sec
* teaching-018 → 1800 sec

It accumulates durations until it finds where `4645 sec` lands.

Example:

```text
worship-021          0–900
sermon-2026-03-01    900–4020
announcements-005    4020–4320
teaching-018         4320–6120
```

So active item is:

```text
teaching-018
```

Package-relative offset inside current item:

```text
4645 - 4320 = 325 sec
```

---

# 8. Segment Resolution

Assume segment duration is 6 seconds.

Then:

```text
325 / 6 = segment index 54
```

So playback begins around:

```text
seg0054.ts
```

The generator then builds a forward-looking window of segments.

Example:

* seg0054.ts
* seg0055.ts
* seg0056.ts
* seg0057.ts

If the window crosses into the next video, it inserts:

```m3u8
#EXT-X-DISCONTINUITY
```

and continues with the next asset.

---

# 9. Playlist Window Strategy

The generator should not return the entire 4-hour block playlist.

It should return only a **small rolling playback window**.

Recommended window:

* 30–60 seconds of content
* roughly 5–10 segments for 6-second chunks

Example:

```text
targetDuration = 6 sec
window = 10 segments
total visible playback = 60 sec
```

This is enough for smooth playback and keeps playlist generation simple.

---

# 10. Generator Modes

There are two valid implementation models.

## 10.1 Dynamic HTTP Generation

Recommended.

The request:

```text
GET /hls/channel.m3u8
```

causes the service to generate the playlist on demand.

Advantages:

* no constant blob overwrites
* easier debugging
* time-aware generation
* clean architecture

## 10.2 Prewritten Static Playlist

Alternative.

A scheduler job rewrites `channel.m3u8` every few seconds.

Advantages:

* simple static hosting model

Disadvantages:

* more storage churn
* race conditions possible
* harder to debug

For this project, use **dynamic generation**.

---

# 11. Master vs Media Playlist

Recommended design:

* `channel.m3u8` = generated media playlist for the chosen quality
* optional separate master playlist later for adaptive bitrate

Phase 1 can use a single working rendition, for example:

```text
720p
```

Later you can introduce a generated master playlist with variants:

```m3u8
#EXTM3U

#EXT-X-STREAM-INF:BANDWIDTH=2500000
/channel/720p.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=800000
/channel/480p.m3u8
```

---

# 12. Required Metadata

To generate playlists efficiently, the backend should store parsed segment metadata.

Example:

```json
{
  "videoId": "teaching-018",
  "variant": "720p",
  "segmentDuration": 6,
  "segments": [
    { "index": 0, "uri": "/hls/teaching-018/720p/seg0000.ts", "duration": 6.0 },
    { "index": 1, "uri": "/hls/teaching-018/720p/seg0001.ts", "duration": 6.0 }
  ]
}
```

This avoids reparsing `.m3u8` files on every request.

---

# 13. Generator Algorithm

## Step-by-step algorithm

```text
1. Get current time
2. Resolve daily schedule
3. Find active 4-hour block
4. Load assigned package
5. Calculate seconds elapsed within block
6. Walk package items to find active video
7. Resolve active segment index
8. Build playlist window
9. If window crosses item boundary, insert discontinuity
10. Return HLS playlist text
```

---

# 14. Example Pseudocode

```python
def generate_channel_playlist(now):
    schedule = load_schedule(now.date())
    block = find_active_block(schedule, now.time())
    package = load_package(block.package_id)

    elapsed = seconds_since_block_start(block, now)
    active_item, offset_in_item = resolve_package_offset(package, elapsed)

    playlist_segments = build_segment_window(package, active_item, offset_in_item)

    return render_m3u8(playlist_segments)
```

---

# 15. Package Offset Resolver Example

```python
def resolve_package_offset(package, elapsed_sec):
    running = 0

    for item in package["items"]:
        video = get_video(item["videoId"])
        next_running = running + video["durationSec"]

        if elapsed_sec < next_running:
            return item, elapsed_sec - running

        running = next_running

    return package["items"][-1], 0
```

---

# 16. Segment Window Builder Example

```python
def build_segment_window(package, active_item, offset_in_item, window_segments=10):
    result = []

    current_item = active_item
    current_offset = offset_in_item

    while len(result) < window_segments and current_item is not None:
        segs = get_segments_for_item(current_item)
        start_index = int(current_offset // 6)

        for seg in segs[start_index:]:
            result.append({
                "uri": seg["uri"],
                "duration": seg["duration"],
                "discontinuity": False
            })

            if len(result) >= window_segments:
                break

        next_item = get_next_package_item(package, current_item)

        if next_item and len(result) < window_segments:
            result.append({
                "discontinuity": True
            })

        current_item = next_item
        current_offset = 0

    return result
```

---

# 17. HLS Renderer Example

```python
def render_m3u8(entries):
    lines = [
        "#EXTM3U",
        "#EXT-X-VERSION:3",
        "#EXT-X-TARGETDURATION:6",
        "#EXT-X-MEDIA-SEQUENCE:0"
    ]

    for entry in entries:
        if entry.get("discontinuity"):
            lines.append("#EXT-X-DISCONTINUITY")
        else:
            lines.append(f"#EXTINF:{entry['duration']:.1f},")
            lines.append(entry["uri"])

    return "\n".join(lines) + "\n"
```

---

# 18. Sunday Package Resolution

Sunday logic should be handled before normal block lookup completes.

Example rule:

```text
If block package is PKG-SUNDAY-CURRENT
    if current Sunday package exists
        use it
    else
        use PKG-SUNDAY-LAST
```

Example pseudocode:

```python
def resolve_effective_package(package_id, now):
    if package_id == "PKG-SUNDAY-CURRENT":
        if exists_package_for_date("current-sunday", now.date()):
            return load_current_sunday_package(now.date())
        return load_package("PKG-SUNDAY-LAST")

    return load_package(package_id)
```

---

# 19. Edge Cases

The generator must handle the following.

## 19.1 Package shorter than 4 hours

Options:

* loop package content
* pad with filler content
* append fallback package

Recommended: append a predefined filler package.

## 19.2 Missing video

If a referenced video is missing:

* log error
* skip asset
* move to next valid item

## 19.3 Missing schedule

If no schedule exists for the day:

* load default day schedule
* fallback to a safe package

## 19.4 Corrupt HLS asset

If segment list unavailable:

* skip asset
* log incident
* continue playback

---

# 20. Caching Strategy

Because `channel.m3u8` changes frequently, caching must be conservative.

Recommended:

* `channel.m3u8`: very low TTL or no CDN cache
* segment files: long CDN cache
* per-video HLS playlists: moderate cache

Recommended behavior:

| Asset             | Cache Policy              |
| ----------------- | ------------------------- |
| channel.m3u8      | no-cache / very short TTL |
| per-video `.m3u8` | short to medium TTL       |
| `.ts` segments    | long TTL                  |

---

# 21. Logging

The generator should log:

* request timestamp
* resolved block
* resolved package
* active video
* segment start index
* fallback usage
* errors

Example log entry:

```json
{
  "timestamp": "2026-03-08T09:17:25",
  "block": "08:00",
  "package": "PKG-SUNDAY-CURRENT",
  "resolvedPackage": "PKG-SUNDAY-CURRENT-2026-03-08",
  "video": "teaching-018",
  "segmentIndex": 54
}
```

---

# 22. Recommended API Endpoints

## Playback

```text
GET /hls/channel.m3u8
```

## Admin / Diagnostics

```text
GET /api/schedule/today
GET /api/package/:id
GET /api/channel/now
GET /api/channel/debug?time=2026-03-08T09:17:25
```

These diagnostic endpoints are extremely useful during testing.

---

# 23. Suggested Data Model

## videos

```json
{
  "videoId": "sermon-2026-03-01",
  "title": "The Grace of God",
  "durationSec": 3120,
  "variants": {
    "720p": {
      "playlist": "/hls/sermon-2026-03-01/720p/index.m3u8",
      "segments": 520
    }
  }
}
```

## packages

```json
{
  "packageId": "PKG-MORNING-01",
  "items": [
    "devotional-001",
    "worship-003",
    "sermon-2026-02-22"
  ]
}
```

## schedules

```json
{
  "date": "2026-03-08",
  "blocks": [
    { "start": "00:00", "packageId": "PKG-NIGHT-01" },
    { "start": "04:00", "packageId": "PKG-MORNING-01" }
  ]
}
```

---

# 24. Minimal Implementation Plan

## Phase 1

* single quality rendition
* dynamic media playlist generation
* JSON-based storage
* Sunday fallback logic
* debug endpoint

## Phase 2

* adaptive bitrate variants
* DB-backed metadata
* admin UI for packages and schedules
* audit trail

---

# 25. Testing Plan

The generator should be tested with:

* block boundary transitions
* package boundary transitions
* Sunday fallback logic
* missing asset handling
* midnight rollover
* long-running Roku playback

Important test cases:

```text
07:59:59 → previous block
08:00:00 → new block
11:59:59 → last second of block
12:00:00 → next block
```

---

# 26. Summary

The HLS Playlist Generator is the core service that turns:

* encoded videos
* packages
* schedule definitions

into a single Roku playback experience.

Its role is simple:

**Given the current time, generate the correct HLS playlist for the current channel state.**

That design keeps the Roku app simple and places all scheduling logic on the backend where it belongs.

---


**“Backend API + JSON schema spec”** for:

* videos
* packages
* schedules
* admin endpoints
* playlist generator endpoints
