Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,14 @@ Lists all of the Control Rooms and Macros for a system. Also fires a named `Star

Usage: `node macros.mjs`

## Showcase Events (Internet Channel)

See [`showcase-events/`](./showcase-events) for building a live-events feature —
"what's live now / coming up" listings and players — on your own site.

These read live and upcoming events from `GET /cablecastapi/publicsitedata`
(`showcaseEventShows`), which is read-only, needs no authentication, and is the
one showcase feed available on both self-hosted Cablecast and Reflect+ hosted
channels. Includes a browser HLS player, a WordPress shortcode, a no-code iframe
embed, and a shared status helper. See [`showcase-events/README.md`](./showcase-events/README.md).

100 changes: 100 additions & 0 deletions showcase-events/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Showcase Events (Internet Channel)

A **Showcase Event** promotes a live or upcoming stream so it can be featured
ahead of on-demand content. On a Cablecast Internet Channel they fill the "Live
Events" area of the home page. These examples let you build the same thing on
your own site.

## Read events from `publicsitedata`

Everything here is built on a single read-only, no-auth endpoint:

```
GET /cablecastapi/publicsitedata?site={siteId}
```

The response is the full configuration and content for one Internet Channel
site. The live and upcoming events live in the `showcaseEventShows` array, each
already resolved into a show with a title, thumbnail and playback URL:

```jsonc
{
"liveGalleryTitle": "Live Events",
"showcaseEventShows": [
{
"showId": 1187,
"title": "City Council Meeting",
"thumbnailUrl": "/cablecastapi/dynamicthumbnails/8821",
"showcaseEventStatus": "live",
"vodUrl": "https://vod.example.org/showcase-2/1187-showcase-event-42/event.m3u8",
"isLive": true,
"scheduleStartTime": "2026-08-19T18:00:00-04:00",
"liveEventStarted": "2026-08-19T18:00:11-04:00",
"liveBridgeEventStatus": "active"
}
]
}
```

This is the same feed the Cablecast Internet Channel itself renders, so a site
built on it behaves identically to a hosted channel — **including Reflect+**. On
a Reflect+ hosted channel the base path is `/api` instead of `/cablecastapi`
(`GET /api/publicsitedata`); the payload shape is the same.

> Prefer `publicsitedata` for any third-party integration. It is the one showcase
> feed available on both self-hosted Cablecast and Reflect+ hosted channels.

## Event status

Each event carries a server-derived **`showcaseEventStatus`** — read that field
rather than working the status out yourself. The server already applies the
"is it really live / has the encoder come up" rules, and both self-hosted
Cablecast and Reflect+ emit the same values, so you don't have to reimplement any
of it.

| `showcaseEventStatus` | Meaning | Play `vodUrl`? |
|-----------------------|---------|----------------|
| `live` | Streaming now, and confirmed ready. `vodUrl` is the live EVENT playlist. | Yes |
| `upcoming` | Scheduled, not streaming yet. Use `scheduleStartTime` for a countdown or a "starting soon" treatment. | No — keep polling |
| `vod` | The event is over. | Only if a recording/VOD exists |

`showcaseEventStatus` is an **open enum** — plan for values beyond the three above.
Reserved for future use:

| Value | Meaning (once emitted) | What to do |
|-------|------------------------|------------|
| `canceled` | The event was called off and will not air (cancelled before start, or ended without producing a recording). | Terminal, not-live, nothing to play — stop polling; hide the listing or show a "cancelled" note. |
| `error` | The event failed to stream (an encoder or platform error prevented it). | Terminal, not-live, no reliable playback — stop polling; hide the listing or show an "unavailable" note. |

Neither is emitted today. Treat **any** value you don't recognise defensively —
as **not-live** (don't mount a player) — so a new status can never break your
integration.

[`event-status.mjs`](./event-status.mjs) reads `showcaseEventStatus` and adds the
client-side `starting_soon` refinement (the near/far split, from
`scheduleStartTime`), returning `live | starting_soon | upcoming | vod`. The other
examples reuse it (and `wordpress.php` ports it to PHP).

## Files

| File | What it shows |
|------|---------------|
| [`event-status.mjs`](./event-status.mjs) | The status helper the other examples import. |
| [`browser-player.html`](./browser-player.html) | Fetch `publicsitedata`, poll every 15s, mount an HLS player when an event goes live. |
| [`wordpress.php`](./wordpress.php) | A `[cablecast_live_events]` WordPress shortcode listing what's live and upcoming. |
| [`iframe-embed.html`](./iframe-embed.html) | No-code option: iframe the Internet Channel show page and let it handle the whole lifecycle. |

## Playback notes

- `vodUrl` carries the live **EVENT** HLS playlist while an event is live, so
late joiners can scrub back to the start, and the same URL keeps serving the
recording for a while after the event ends. Treat post-event playback as best
effort and handle a failed load.
- Turn on your player's live UI (in video.js, `liveui: true` with source type
`application/x-mpegURL`) so the scrubber and "back to live" control appear.
- Captions and translated subtitle tracks travel inside the manifest as subtitle
renditions; players pick them up automatically.
- Poll `publicsitedata` no faster than its 15-second cache. Stop polling once
you've mounted the player — the manifest keeps the stream current on its own.
- For a durable on-demand copy after the event, the station publishes a normal
VOD for the show, available through the usual `vods` endpoints.
122 changes: 122 additions & 0 deletions showcase-events/browser-player.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<!doctype html>
<!--
Minimal live-events player built on `GET /cablecastapi/publicsitedata`.

It reads the site's `showcaseEventShows`, finds one that is live, and mounts an
HLS player against its `vodUrl` (which carries the live EVENT playlist while an
event is live). While nothing is live it keeps polling on the same 15-second
cadence the Cablecast Internet Channel uses.

`publicsitedata` is read-only, needs no authentication, and sends open CORS
headers, so this works straight from the browser. Serve this file over http(s)
(not file://) so the ES-module import and the cross-origin fetch both work.

Set API_BASE and SITE_ID below for your system:
- Self-hosted Cablecast: https://your-server.example.org/cablecastapi
- Reflect+ hosted: your Reflect+ channel origin + /api (path is /api,
not /cablecastapi; the payload shape is identical)
-->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Cablecast Live Events</title>
<!--
This example loads video.js from a public CDN to stay copy-paste simple. For
production, self-host these assets or add Subresource Integrity (integrity + crossorigin)
to the pinned version so a compromised CDN can't inject script.
-->
<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />
<script src="https://vjs.zencdn.net/8.10.0/video.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; }
#status { margin-bottom: 1rem; color: #444; }
.video-js { width: 100%; max-width: 800px; aspect-ratio: 16 / 9; height: auto; }
</style>
</head>
<body>
<p id="status">Checking for live events…</p>
<video id="player" class="video-js" controls playsinline poster=""></video>

<script type="module">
import { getEventStatus } from "./event-status.mjs";

// ---- configure for your system -------------------------------------
const API_BASE = "https://cablecast.example.org/cablecastapi";
const SITE_ID = 1;
const POLL_MS = 15000; // matches the 15s publicsitedata cache
// --------------------------------------------------------------------

const statusEl = document.getElementById("status");
let player = null;

// `thumbnailUrl` is a path relative to the Cablecast host, so prefix it
// with the origin of API_BASE.
const apiOrigin = new URL(API_BASE).origin;
const absolute = (path) =>
path ? (path.startsWith("http") ? path : apiOrigin + path) : "";

async function tick() {
let config;
try {
const res = await fetch(`${API_BASE}/publicsitedata?site=${SITE_ID}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
config = await res.json();
} catch (err) {
// Fail soft: leave any running player alone and try again shortly.
statusEl.textContent = "Could not reach the Cablecast API. Retrying…";
return setTimeout(tick, POLL_MS);
}

const shows = config.showcaseEventShows ?? [];
const live = shows.find((s) => getEventStatus(s) === "live");

if (live) {
mount(live);
// Stop polling once mounted: the HLS manifest keeps the stream current
// on its own, and re-reading a cached response can briefly flip the
// status and tear the player down.
return;
}

// Nothing on right now. Surface the next upcoming event, if any. Guard against
// a missing/invalid scheduleStartTime so the sort never compares NaN (which
// leaves order undefined) and we never render "Invalid Date".
const startMs = (s) => {
const t = new Date(s.scheduleStartTime).getTime();
return Number.isNaN(t) ? Infinity : t;
};
const upcoming = shows
.filter((s) => ["starting_soon", "upcoming"].includes(getEventStatus(s)))
.sort((a, b) => startMs(a) - startMs(b))[0];

const upcomingStart = upcoming ? startMs(upcoming) : Infinity;
statusEl.textContent = upcoming
? Number.isFinite(upcomingStart)
? `Next up: ${upcoming.title} at ${new Date(
upcomingStart
).toLocaleString()}`
: `Next up: ${upcoming.title}`
: "No live events scheduled right now.";

setTimeout(tick, POLL_MS);
}

function mount(show) {
statusEl.textContent = `Live now: ${show.title}`;
const el = document.getElementById("player");
el.poster = absolute(show.thumbnailUrl);

// `vodUrl` carries the live EVENT playlist while the event is live, so
// late joiners can scrub back to the start. Captions and translated
// subtitle tracks ride inside the manifest — no separate fetch needed.
player = videojs("player", {
liveui: true,
sources: [{ src: show.vodUrl, type: "application/x-mpegURL" }],
});
}

tick();
</script>
</body>
</html>
59 changes: 59 additions & 0 deletions showcase-events/event-status.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Resolves the display status of a Showcase Event from a `showcaseEventShows`
// entry returned by `GET /cablecastapi/publicsitedata`.
//
// The server already decides the status and returns it as `showcaseEventStatus`:
// the platform applies the "is it really live / has the encoder come up" rules,
// and both self-hosted Cablecast and Reflect+ hosted channels emit the same
// values, so a third-party site behaves identically to a hosted channel. This
// helper just reads that field and adds the cosmetic near/far "starting soon"
// split, which is a client-side choice.

export const STARTING_SOON_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes

/**
* @param {object} show One entry from `config.showcaseEventShows`.
* @param {number} [now] Milliseconds since epoch; defaults to now. Injectable
* so the same call is testable.
* @returns {"live"|"starting_soon"|"upcoming"|"vod"}
*/
export function getEventStatus(show, now = Date.now()) {
// `showcaseEventStatus` is an OPEN enum. Emitted today: "live" | "upcoming" |
// "vod". Reserved for future use: "canceled" (event called off, will not air)
// and "error" (event failed to stream) — both are terminal, not-live states
// with nothing to play. Handle any value you don't recognise defensively by
// treating it as not-live, so a new status can never break your integration.
switch (show.showcaseEventStatus) {
case "live":
return "live";
case "upcoming":
return refineUpcoming(show, now);
default:
// "vod", "canceled", "error", or anything unrecognised: not live.
return "vod";
}
}
Comment thread
raytiley marked this conversation as resolved.

/**
* Turns the server's `upcoming` into the near/far split the UI wants. The server
* only ever sends `upcoming`; whether to show a "starting soon" treatment is a
* client decision made here from `scheduleStartTime`.
*/
function refineUpcoming(show, now) {
if (show.scheduleStartTime) {
const startTime = new Date(show.scheduleStartTime).getTime();
if (!Number.isNaN(startTime)) {
// "Starting soon" is a window around the scheduled start: within the threshold
// before it, or just after it (the server can still report `upcoming` briefly
// past the start). A start far in the past is not "soon", so leave it as plain
// `upcoming` rather than showing "starting soon" indefinitely.
const untilStart = startTime - now;
if (
untilStart <= STARTING_SOON_THRESHOLD_MS &&
untilStart >= -STARTING_SOON_THRESHOLD_MS
) {
return "starting_soon";
}
}
}
return "upcoming";
}
33 changes: 33 additions & 0 deletions showcase-events/iframe-embed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!doctype html>
<!--
No-code option: embed the Internet Channel show page in an iframe.

If you would rather not build a player, iframe the show page. It already
handles the whole event lifecycle for you — the poster while an event is
upcoming, the live player when it starts, and the recording afterwards — and
needs no JavaScript.

You need the Show ID, which is the `showId` field on a showcaseEventShows
entry from `GET /cablecastapi/publicsitedata`. Point the iframe at that show on
your channel's public site and keep a 16:9 aspect ratio. See the Cablecast
support article "Embedding Video on Third-party Websites" for sizing details.
-->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Cablecast Show Embed</title>
</head>
<body>
<!-- Replace the host with your channel's public site and 1187 with a showId. -->
<iframe
src="https://yourtown.cablecast.tv/show/1187"
width="800"
height="450"
frameborder="0"
allow="autoplay; fullscreen"
allowfullscreen
title="City Council Meeting"
></iframe>
</body>
</html>
Loading