diff --git a/README.md b/README.md index b9874a1..40adf9b 100644 --- a/README.md +++ b/README.md @@ -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). + diff --git a/showcase-events/README.md b/showcase-events/README.md new file mode 100644 index 0000000..be1427d --- /dev/null +++ b/showcase-events/README.md @@ -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. diff --git a/showcase-events/browser-player.html b/showcase-events/browser-player.html new file mode 100644 index 0000000..e4c188c --- /dev/null +++ b/showcase-events/browser-player.html @@ -0,0 +1,122 @@ + + + +
+ + +Checking for live events…
+ + + + + diff --git a/showcase-events/event-status.mjs b/showcase-events/event-status.mjs new file mode 100644 index 0000000..a38fa34 --- /dev/null +++ b/showcase-events/event-status.mjs @@ -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"; + } +} + +/** + * 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"; +} diff --git a/showcase-events/iframe-embed.html b/showcase-events/iframe-embed.html new file mode 100644 index 0000000..fac2cdd --- /dev/null +++ b/showcase-events/iframe-embed.html @@ -0,0 +1,33 @@ + + + + + + +No live events scheduled right now.
'; + } + + // thumbnailUrl is a path relative to the Cablecast host. Strip whichever API base + // path is configured — /cablecastapi on self-hosted, /api on Reflect+ — tolerating a + // trailing slash in the configured value. + $host = preg_replace( '#/(cablecastapi|api)$#', '', rtrim( CABLECAST_API, '/' ) ); + $out = '