From ab1850de6a709c16e42e684541d1deb04864a624 Mon Sep 17 00:00:00 2001 From: Ray Tiley Date: Mon, 24 Aug 2026 13:13:36 -0400 Subject: [PATCH 1/7] Add showcase-events examples built on publicsitedata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Building a live-events feature (what's live now / coming up listings and players) on a third-party site. Everything reads live and upcoming events from GET /cablecastapi/publicsitedata (showcaseEventShows) — the one showcase feed available on both self-hosted Cablecast and Reflect+ hosted channels. - event-status.mjs: status helper mirroring the Internet Channel's getEventStatus (live / starting_soon / upcoming / vod), incl. the LiveBridge "wait for active" nuance - browser-player.html: fetch, poll every 15s, mount an HLS player when live - wordpress.php: [cablecast_live_events] shortcode - iframe-embed.html: no-code embed of the show page - README for the folder + a section in the top-level README Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 11 +++ showcase-events/README.md | 87 ++++++++++++++++ showcase-events/browser-player.html | 111 +++++++++++++++++++++ showcase-events/event-status.mjs | 62 ++++++++++++ showcase-events/iframe-embed.html | 33 +++++++ showcase-events/wordpress.php | 148 ++++++++++++++++++++++++++++ 6 files changed, 452 insertions(+) create mode 100644 showcase-events/README.md create mode 100644 showcase-events/browser-player.html create mode 100644 showcase-events/event-status.mjs create mode 100644 showcase-events/iframe-embed.html create mode 100644 showcase-events/wordpress.php 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..dcfbc1b --- /dev/null +++ b/showcase-events/README.md @@ -0,0 +1,87 @@ +# 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", + "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. + +## Deriving status + +`publicsitedata` does not give you a single `status` string. You compute one +from `isLive`, `scheduleStartTime`, and `liveBridgeEventStatus`. The important +rule: an event is only truly ready to play once the stream is confirmed running — +don't mount a player just because the scheduled start time has passed, or you'll +hit a manifest that 404s. + +[`event-status.mjs`](./event-status.mjs) implements exactly the logic the +Internet Channel uses, returning `live | starting_soon | upcoming | vod`. The +other examples reuse it (and `wordpress.php` ports it to PHP). + +| status | Meaning | Play `vodUrl`? | +|--------|---------|----------------| +| `live` | Streaming now. `vodUrl` is the live EVENT playlist. | Yes | +| `starting_soon` | Within 15 min of start, or started but the encoder isn't up yet. | No — keep polling | +| `upcoming` | More than 15 min out. | No | +| `vod` | Over, or no event pending. | Only if a recording/VOD exists | + +## 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..4f54c6d --- /dev/null +++ b/showcase-events/browser-player.html @@ -0,0 +1,111 @@ + + + + + + + Cablecast Live Events + + + + + +

Checking for live events…

+ + + + + diff --git a/showcase-events/event-status.mjs b/showcase-events/event-status.mjs new file mode 100644 index 0000000..9744481 --- /dev/null +++ b/showcase-events/event-status.mjs @@ -0,0 +1,62 @@ +// Derives the display status of a Showcase Event from a `showcaseEventShows` +// entry returned by `GET /cablecastapi/publicsitedata`. +// +// This mirrors the logic the Cablecast Internet Channel itself uses, so a +// third-party site built on `publicsitedata` behaves identically to a hosted +// channel (including Reflect+). `publicsitedata` does not hand you a single +// `status` string — you compute it from `isLive`, `scheduleStartTime`, and +// `liveBridgeEventStatus`, which is exactly what this helper does. + +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()) { + const bridgeStatus = show.liveBridgeEventStatus?.toLowerCase() ?? null; + + // How aggressively we trust `isLive` to mean "ready to play" depends on the + // stream URL: + // - /showcase recordings publish their EVENT playlist as soon as isLive + // flips, so isLive alone is a safe ready signal. + // - /livebridge streams only serve a manifest once the encoder is up, which + // LiveBridge reports as status "active". isLive can flip true before any + // segments exist, so mounting a player on isLive alone would 404. For those, + // require "active". + const eventUrl = `${show.vodUrl ?? ""} ${show.liveStreamUrl ?? ""}`; + const isLiveBridgeStream = + /\/livebridge/i.test(eventUrl) && !/\/showcase/i.test(eventUrl); + + if (show.isLive && !isLiveBridgeStream) return "live"; + + // "active" means the encoder is up and the manifest is serving — the ready + // signal for /livebridge streams, and an override for a stale isLive=false + // from a cached /showcase response. + if (bridgeStatus === "active") return "live"; + + if (show.scheduleStartTime) { + const startTime = new Date(show.scheduleStartTime).getTime(); + if (!Number.isNaN(startTime)) { + if (startTime > now) { + return startTime - now <= STARTING_SOON_THRESHOLD_MS + ? "starting_soon" + : "upcoming"; + } + // Scheduled start has passed but the stream is not confirmed live yet. + // Keep it "starting soon" (rather than dropping to "vod") while LiveBridge + // still reports scheduled/starting, or inside a 15-minute grace window. + if (bridgeStatus === "scheduled" || bridgeStatus === "starting") { + return "starting_soon"; + } + if (now - startTime <= STARTING_SOON_THRESHOLD_MS) { + return "starting_soon"; + } + } + return "vod"; + } + + return "vod"; +} 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 @@ + + + + + + + Cablecast Show Embed + + + + + + diff --git a/showcase-events/wordpress.php b/showcase-events/wordpress.php new file mode 100644 index 0000000..695b63f --- /dev/null +++ b/showcase-events/wordpress.php @@ -0,0 +1,148 @@ + $now ) { + return ( $start - $now ) <= CABLECAST_STARTING_SOON_THRESHOLD + ? 'starting_soon' + : 'upcoming'; + } + if ( 'scheduled' === $bridge_status || 'starting' === $bridge_status ) { + return 'starting_soon'; + } + if ( ( $now - $start ) <= CABLECAST_STARTING_SOON_THRESHOLD ) { + return 'starting_soon'; + } + } + return 'vod'; + } + + return 'vod'; +} + +/** + * Fetch the site's showcase event shows, cached for 15 seconds. + * + * @return array list of showcaseEventShows entries (may be empty) + */ +function cablecast_get_showcase_event_shows() { + $cache_key = 'cablecast_showcase_shows'; + $cached = get_transient( $cache_key ); + if ( false !== $cached ) { + return $cached; + } + + $url = add_query_arg( + array( 'site' => CABLECAST_SITE_ID ), + CABLECAST_API . '/publicsitedata' + ); + $response = wp_remote_get( $url, array( 'timeout' => 5 ) ); + + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + // Non-200 means the feed could not be read; fail soft so the page renders. + return array(); + } + + $body = json_decode( wp_remote_retrieve_body( $response ), true ); + $shows = isset( $body['showcaseEventShows'] ) ? $body['showcaseEventShows'] : array(); + + set_transient( $cache_key, $shows, 15 ); + return $shows; +} + +/** + * [cablecast_live_events] - renders what is streaming now and what is next. + */ +function cablecast_live_events_shortcode() { + $shows = cablecast_get_showcase_event_shows(); + + $live = array(); + $upcoming = array(); + foreach ( $shows as $show ) { + $status = cablecast_event_status( $show ); + if ( 'live' === $status ) { + $live[] = $show; + } elseif ( 'starting_soon' === $status || 'upcoming' === $status ) { + $upcoming[] = $show; + } + } + + if ( empty( $live ) && empty( $upcoming ) ) { + return '

No live events scheduled right now.

'; + } + + // thumbnailUrl is a path relative to the Cablecast host. + $host = preg_replace( '#/cablecastapi$#', '', CABLECAST_API ); + $out = ''; +} +add_shortcode( 'cablecast_live_events', 'cablecast_live_events_shortcode' ); From a971c34caa2e8023dd4f14658e1ec2658008230d Mon Sep 17 00:00:00 2001 From: Ray Tiley Date: Mon, 24 Aug 2026 14:26:48 -0400 Subject: [PATCH 2/7] showcase-events: read the server-derived showcaseEventStatus field publicsitedata now carries an authoritative showcaseEventStatus on each event ("live" | "upcoming" | "vod", an open enum with "canceled"/"error" reserved). Read it instead of re-deriving status from isLive/scheduleStartTime/liveBridgeEventStatus. - event-status.mjs: getEventStatus prefers showcaseEventStatus and adds only the client-side "starting soon" refinement; deriveEventStatus stays as a fallback for older servers. Unknown/reserved values are treated as not-live. - wordpress.php: same preference + fallback (cablecast_derive_event_status). - README: documents showcaseEventStatus as the field to read, the open-enum contract, and the reserved values. Co-Authored-By: Claude Opus 4.8 (1M context) --- showcase-events/README.md | 44 ++++++++++++++++------------ showcase-events/event-status.mjs | 49 ++++++++++++++++++++++++++++---- showcase-events/wordpress.php | 43 +++++++++++++++++++++++++--- 3 files changed, 108 insertions(+), 28 deletions(-) diff --git a/showcase-events/README.md b/showcase-events/README.md index dcfbc1b..4706f11 100644 --- a/showcase-events/README.md +++ b/showcase-events/README.md @@ -25,6 +25,7 @@ already resolved into a show with a title, thumbnail and playback URL: "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", @@ -43,24 +44,31 @@ a Reflect+ hosted channel the base path is `/api` instead of `/cablecastapi` > Prefer `publicsitedata` for any third-party integration. It is the one showcase > feed available on both self-hosted Cablecast and Reflect+ hosted channels. -## Deriving status - -`publicsitedata` does not give you a single `status` string. You compute one -from `isLive`, `scheduleStartTime`, and `liveBridgeEventStatus`. The important -rule: an event is only truly ready to play once the stream is confirmed running — -don't mount a player just because the scheduled start time has passed, or you'll -hit a manifest that 404s. - -[`event-status.mjs`](./event-status.mjs) implements exactly the logic the -Internet Channel uses, returning `live | starting_soon | upcoming | vod`. The -other examples reuse it (and `wordpress.php` ports it to PHP). - -| status | Meaning | Play `vodUrl`? | -|--------|---------|----------------| -| `live` | Streaming now. `vodUrl` is the live EVENT playlist. | Yes | -| `starting_soon` | Within 15 min of start, or started but the encoder isn't up yet. | No — keep polling | -| `upcoming` | More than 15 min out. | No | -| `vod` | Over, or no event pending. | Only if a recording/VOD exists | +## 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**: only `live`/`upcoming`/`vod` are sent +today, but `canceled` and `error` are reserved and may appear in future. Handle +any value you don't recognise defensively — treat it 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`. It also +falls back to deriving the status from `isLive`/`liveBridgeEventStatus` for older +servers that don't send the field yet, so it's safe against any Cablecast +version. The other examples reuse it (and `wordpress.php` ports it to PHP). ## Files diff --git a/showcase-events/event-status.mjs b/showcase-events/event-status.mjs index 9744481..507e61c 100644 --- a/showcase-events/event-status.mjs +++ b/showcase-events/event-status.mjs @@ -1,11 +1,13 @@ -// Derives the display status of a Showcase Event from a `showcaseEventShows` +// Resolves the display status of a Showcase Event from a `showcaseEventShows` // entry returned by `GET /cablecastapi/publicsitedata`. // -// This mirrors the logic the Cablecast Internet Channel itself uses, so a -// third-party site built on `publicsitedata` behaves identically to a hosted -// channel (including Reflect+). `publicsitedata` does not hand you a single -// `status` string — you compute it from `isLive`, `scheduleStartTime`, and -// `liveBridgeEventStatus`, which is exactly what this helper does. +// Prefer the server-derived `showcaseEventStatus` field: the platform already +// 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 reads +// that field and adds only the cosmetic near/far "starting soon" split, which is +// a client-side choice. For older servers that predate the field it falls back +// to deriving the status from `isLive`/`scheduleStartTime`/`liveBridgeEventStatus`. export const STARTING_SOON_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes @@ -16,6 +18,41 @@ export const STARTING_SOON_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes * @returns {"live"|"starting_soon"|"upcoming"|"vod"} */ export function getEventStatus(show, now = Date.now()) { + // `showcaseEventStatus` is an OPEN enum: "live" | "upcoming" | "vod" today, + // with "canceled"/"error" reserved for future use. Handle unrecognised values + // defensively — never assume an unknown value means live. + const serverStatus = show.showcaseEventStatus; + if (serverStatus) { + if (serverStatus === "live") return "live"; + if (serverStatus === "upcoming") return refineUpcoming(show, now); + // "vod", plus any reserved/unknown value: treat as not-live. + return "vod"; + } + + return deriveEventStatus(show, now); +} + +/** + * 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) && startTime - now <= STARTING_SOON_THRESHOLD_MS) { + return "starting_soon"; + } + } + return "upcoming"; +} + +/** + * Fallback for older Cablecast servers that don't send `showcaseEventStatus` + * yet. Mirrors the logic the Internet Channel used before the field existed. + * Once every deployment sends the field, this can be deleted. + */ +export function deriveEventStatus(show, now = Date.now()) { const bridgeStatus = show.liveBridgeEventStatus?.toLowerCase() ?? null; // How aggressively we trust `isLive` to mean "ready to play" depends on the diff --git a/showcase-events/wordpress.php b/showcase-events/wordpress.php index 695b63f..5044445 100644 --- a/showcase-events/wordpress.php +++ b/showcase-events/wordpress.php @@ -4,8 +4,8 @@ * * Drop this in your theme's functions.php or a small plugin, then use the * [cablecast_live_events] shortcode in any page or widget. It reads the site's - * `showcaseEventShows`, derives each event's status the same way the Cablecast - * Internet Channel does, and lists what is live now and what is coming up. + * `showcaseEventShows`, uses the server-derived `showcaseEventStatus` on each + * event, and lists what is live now and what is coming up. * * The transient keeps you inside the 15-second publicsitedata cache window, so a * burst of traffic does not turn into a burst of API calls. @@ -20,12 +20,47 @@ const CABLECAST_STARTING_SOON_THRESHOLD = 15 * 60; // seconds /** - * Derive the display status of one showcaseEventShows entry. - * Mirrors event-status.mjs — see that file for the reasoning behind each branch. + * Resolve the display status of one showcaseEventShows entry. + * + * Prefers the server-derived `showcaseEventStatus` — an OPEN enum that is + * "live" | "upcoming" | "vod" today, with "canceled"/"error" reserved. Unknown + * values are treated defensively as not-live. Only the near/far "starting soon" + * split is decided here, from scheduleStartTime. Falls back to deriving the + * status for older servers that do not send the field. Mirrors event-status.mjs. * * @return string one of live|starting_soon|upcoming|vod */ function cablecast_event_status( $show, $now = null ) { + $now = $now ?? time(); + + $server_status = $show['showcaseEventStatus'] ?? null; + if ( ! empty( $server_status ) ) { + if ( 'live' === $server_status ) { + return 'live'; + } + if ( 'upcoming' === $server_status ) { + if ( ! empty( $show['scheduleStartTime'] ) ) { + $start = strtotime( $show['scheduleStartTime'] ); + if ( false !== $start && ( $start - $now ) <= CABLECAST_STARTING_SOON_THRESHOLD ) { + return 'starting_soon'; + } + } + return 'upcoming'; + } + // "vod", plus any reserved/unknown value: treat as not-live. + return 'vod'; + } + + return cablecast_derive_event_status( $show, $now ); +} + +/** + * Fallback for older servers with no `showcaseEventStatus`. See event-status.mjs + * for the reasoning behind each branch. + * + * @return string one of live|starting_soon|upcoming|vod + */ +function cablecast_derive_event_status( $show, $now = null ) { $now = $now ?? time(); $bridge_status = isset( $show['liveBridgeEventStatus'] ) ? strtolower( (string) $show['liveBridgeEventStatus'] ) From 70dbff3bc6f342d5dc5a8db049551fd963d255f3 Mon Sep 17 00:00:00 2001 From: Ray Tiley Date: Mon, 24 Aug 2026 15:29:51 -0400 Subject: [PATCH 3/7] showcase-events: drop the unused derive fallback; document canceled/error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These examples only ever run over showcaseEventShows entries, which always carry showcaseEventStatus, and showcase events haven't shipped — so there is no older server to derive for. Remove the dead deriveEventStatus / cablecast_derive_event_status paths; getEventStatus now just reads the field and adds the starting_soon refinement. Document what the reserved values will mean once emitted: canceled (event called off, will not air) and error (failed to stream) — both terminal not-live states with nothing to play; stop polling and hide or show an unavailable note. Co-Authored-By: Claude Opus 4.8 (1M context) --- showcase-events/README.md | 21 +++++--- showcase-events/event-status.mjs | 88 +++++++------------------------- showcase-events/wordpress.php | 71 +++++--------------------- 3 files changed, 44 insertions(+), 136 deletions(-) diff --git a/showcase-events/README.md b/showcase-events/README.md index 4706f11..be1427d 100644 --- a/showcase-events/README.md +++ b/showcase-events/README.md @@ -58,17 +58,22 @@ of it. | `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**: only `live`/`upcoming`/`vod` are sent -today, but `canceled` and `error` are reserved and may appear in future. Handle -any value you don't recognise defensively — treat it as **not-live** (don't mount -a player), so a new status can never break your integration. +`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`. It also -falls back to deriving the status from `isLive`/`liveBridgeEventStatus` for older -servers that don't send the field yet, so it's safe against any Cablecast -version. The other examples reuse it (and `wordpress.php` ports it to PHP). +`scheduleStartTime`), returning `live | starting_soon | upcoming | vod`. The other +examples reuse it (and `wordpress.php` ports it to PHP). ## Files diff --git a/showcase-events/event-status.mjs b/showcase-events/event-status.mjs index 507e61c..393e4d4 100644 --- a/showcase-events/event-status.mjs +++ b/showcase-events/event-status.mjs @@ -1,13 +1,12 @@ // Resolves the display status of a Showcase Event from a `showcaseEventShows` // entry returned by `GET /cablecastapi/publicsitedata`. // -// Prefer the server-derived `showcaseEventStatus` field: the platform already -// 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 reads -// that field and adds only the cosmetic near/far "starting soon" split, which is -// a client-side choice. For older servers that predate the field it falls back -// to deriving the status from `isLive`/`scheduleStartTime`/`liveBridgeEventStatus`. +// 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 @@ -18,18 +17,20 @@ export const STARTING_SOON_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes * @returns {"live"|"starting_soon"|"upcoming"|"vod"} */ export function getEventStatus(show, now = Date.now()) { - // `showcaseEventStatus` is an OPEN enum: "live" | "upcoming" | "vod" today, - // with "canceled"/"error" reserved for future use. Handle unrecognised values - // defensively — never assume an unknown value means live. - const serverStatus = show.showcaseEventStatus; - if (serverStatus) { - if (serverStatus === "live") return "live"; - if (serverStatus === "upcoming") return refineUpcoming(show, now); - // "vod", plus any reserved/unknown value: treat as not-live. - return "vod"; + // `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"; } - - return deriveEventStatus(show, now); } /** @@ -46,54 +47,3 @@ function refineUpcoming(show, now) { } return "upcoming"; } - -/** - * Fallback for older Cablecast servers that don't send `showcaseEventStatus` - * yet. Mirrors the logic the Internet Channel used before the field existed. - * Once every deployment sends the field, this can be deleted. - */ -export function deriveEventStatus(show, now = Date.now()) { - const bridgeStatus = show.liveBridgeEventStatus?.toLowerCase() ?? null; - - // How aggressively we trust `isLive` to mean "ready to play" depends on the - // stream URL: - // - /showcase recordings publish their EVENT playlist as soon as isLive - // flips, so isLive alone is a safe ready signal. - // - /livebridge streams only serve a manifest once the encoder is up, which - // LiveBridge reports as status "active". isLive can flip true before any - // segments exist, so mounting a player on isLive alone would 404. For those, - // require "active". - const eventUrl = `${show.vodUrl ?? ""} ${show.liveStreamUrl ?? ""}`; - const isLiveBridgeStream = - /\/livebridge/i.test(eventUrl) && !/\/showcase/i.test(eventUrl); - - if (show.isLive && !isLiveBridgeStream) return "live"; - - // "active" means the encoder is up and the manifest is serving — the ready - // signal for /livebridge streams, and an override for a stale isLive=false - // from a cached /showcase response. - if (bridgeStatus === "active") return "live"; - - if (show.scheduleStartTime) { - const startTime = new Date(show.scheduleStartTime).getTime(); - if (!Number.isNaN(startTime)) { - if (startTime > now) { - return startTime - now <= STARTING_SOON_THRESHOLD_MS - ? "starting_soon" - : "upcoming"; - } - // Scheduled start has passed but the stream is not confirmed live yet. - // Keep it "starting soon" (rather than dropping to "vod") while LiveBridge - // still reports scheduled/starting, or inside a 15-minute grace window. - if (bridgeStatus === "scheduled" || bridgeStatus === "starting") { - return "starting_soon"; - } - if (now - startTime <= STARTING_SOON_THRESHOLD_MS) { - return "starting_soon"; - } - } - return "vod"; - } - - return "vod"; -} diff --git a/showcase-events/wordpress.php b/showcase-events/wordpress.php index 5044445..9fdf0fc 100644 --- a/showcase-events/wordpress.php +++ b/showcase-events/wordpress.php @@ -22,23 +22,22 @@ /** * Resolve the display status of one showcaseEventShows entry. * - * Prefers the server-derived `showcaseEventStatus` — an OPEN enum that is - * "live" | "upcoming" | "vod" today, with "canceled"/"error" reserved. Unknown - * values are treated defensively as not-live. Only the near/far "starting soon" - * split is decided here, from scheduleStartTime. Falls back to deriving the - * status for older servers that do not send the field. Mirrors event-status.mjs. + * Reads the server-derived `showcaseEventStatus` — 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 terminal, + * not-live, nothing to play. Unrecognised values are treated defensively as + * not-live. Only the near/far "starting soon" split is decided here, from + * scheduleStartTime. Mirrors event-status.mjs. * * @return string one of live|starting_soon|upcoming|vod */ function cablecast_event_status( $show, $now = null ) { $now = $now ?? time(); - $server_status = $show['showcaseEventStatus'] ?? null; - if ( ! empty( $server_status ) ) { - if ( 'live' === $server_status ) { + switch ( $show['showcaseEventStatus'] ?? null ) { + case 'live': return 'live'; - } - if ( 'upcoming' === $server_status ) { + case 'upcoming': if ( ! empty( $show['scheduleStartTime'] ) ) { $start = strtotime( $show['scheduleStartTime'] ); if ( false !== $start && ( $start - $now ) <= CABLECAST_STARTING_SOON_THRESHOLD ) { @@ -46,56 +45,10 @@ function cablecast_event_status( $show, $now = null ) { } } return 'upcoming'; - } - // "vod", plus any reserved/unknown value: treat as not-live. - return 'vod'; - } - - return cablecast_derive_event_status( $show, $now ); -} - -/** - * Fallback for older servers with no `showcaseEventStatus`. See event-status.mjs - * for the reasoning behind each branch. - * - * @return string one of live|starting_soon|upcoming|vod - */ -function cablecast_derive_event_status( $show, $now = null ) { - $now = $now ?? time(); - $bridge_status = isset( $show['liveBridgeEventStatus'] ) - ? strtolower( (string) $show['liveBridgeEventStatus'] ) - : null; - - $event_url = ( $show['vodUrl'] ?? '' ) . ' ' . ( $show['liveStreamUrl'] ?? '' ); - $is_livebridge_stream = - preg_match( '#/livebridge#i', $event_url ) && ! preg_match( '#/showcase#i', $event_url ); - - if ( ! empty( $show['isLive'] ) && ! $is_livebridge_stream ) { - return 'live'; - } - if ( 'active' === $bridge_status ) { - return 'live'; + default: + // "vod", "canceled", "error", or anything unrecognised: not live. + return 'vod'; } - - if ( ! empty( $show['scheduleStartTime'] ) ) { - $start = strtotime( $show['scheduleStartTime'] ); - if ( false !== $start ) { - if ( $start > $now ) { - return ( $start - $now ) <= CABLECAST_STARTING_SOON_THRESHOLD - ? 'starting_soon' - : 'upcoming'; - } - if ( 'scheduled' === $bridge_status || 'starting' === $bridge_status ) { - return 'starting_soon'; - } - if ( ( $now - $start ) <= CABLECAST_STARTING_SOON_THRESHOLD ) { - return 'starting_soon'; - } - } - return 'vod'; - } - - return 'vod'; } /** From a38c61698d318751794cf16fc43d7e733df6415e Mon Sep 17 00:00:00 2001 From: Ray Tiley Date: Mon, 24 Aug 2026 16:32:38 -0400 Subject: [PATCH 4/7] =?UTF-8?q?Address=20PR=20review=20=E2=80=94=20harden?= =?UTF-8?q?=20WordPress=20+=20browser=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wordpress.php: guard the define()s so pasting into functions.php twice does not warn; strip either /cablecastapi or /api from the host for thumbnails (Reflect+ uses /api); guard strtotime against a missing/invalid scheduleStartTime. - event-status.mjs / wordpress.php: bound the "starting soon" refinement on the past side so a far-past "upcoming" is not shown as starting-soon forever. - browser-player.html: guard the upcoming sort against missing/invalid scheduleStartTime so it never compares NaN or renders "Invalid Date". Co-Authored-By: Claude Opus 4.8 (1M context) --- showcase-events/browser-player.html | 22 +++++++++++------ showcase-events/event-status.mjs | 14 +++++++++-- showcase-events/wordpress.php | 38 +++++++++++++++++++++-------- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/showcase-events/browser-player.html b/showcase-events/browser-player.html index 4f54c6d..aa91c9c 100644 --- a/showcase-events/browser-player.html +++ b/showcase-events/browser-player.html @@ -74,18 +74,24 @@ return; } - // Nothing on right now. Surface the next upcoming event, if any. + // 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) => - new Date(a.scheduleStartTime) - new Date(b.scheduleStartTime) - )[0]; + .sort((a, b) => startMs(a) - startMs(b))[0]; + const upcomingStart = upcoming ? startMs(upcoming) : Infinity; statusEl.textContent = upcoming - ? `Next up: ${upcoming.title} at ${new Date( - upcoming.scheduleStartTime - ).toLocaleString()}` + ? 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); diff --git a/showcase-events/event-status.mjs b/showcase-events/event-status.mjs index 393e4d4..a38fa34 100644 --- a/showcase-events/event-status.mjs +++ b/showcase-events/event-status.mjs @@ -41,8 +41,18 @@ export function getEventStatus(show, now = Date.now()) { function refineUpcoming(show, now) { if (show.scheduleStartTime) { const startTime = new Date(show.scheduleStartTime).getTime(); - if (!Number.isNaN(startTime) && startTime - now <= STARTING_SOON_THRESHOLD_MS) { - return "starting_soon"; + 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/wordpress.php b/showcase-events/wordpress.php index 9fdf0fc..7f7dad2 100644 --- a/showcase-events/wordpress.php +++ b/showcase-events/wordpress.php @@ -14,10 +14,17 @@ * channel the base path is /api instead of /cablecastapi; the payload is the same. */ -define( 'CABLECAST_API', 'https://cablecast.example.org/cablecastapi' ); -define( 'CABLECAST_SITE_ID', 1 ); - -const CABLECAST_STARTING_SOON_THRESHOLD = 15 * 60; // seconds +// Guarded so pasting this into functions.php alongside other config (or including +// it more than once) does not raise "constant already defined" warnings. +if ( ! defined( 'CABLECAST_API' ) ) { + define( 'CABLECAST_API', 'https://cablecast.example.org/cablecastapi' ); +} +if ( ! defined( 'CABLECAST_SITE_ID' ) ) { + define( 'CABLECAST_SITE_ID', 1 ); +} +if ( ! defined( 'CABLECAST_STARTING_SOON_THRESHOLD' ) ) { + define( 'CABLECAST_STARTING_SOON_THRESHOLD', 15 * 60 ); // seconds +} /** * Resolve the display status of one showcaseEventShows entry. @@ -40,8 +47,15 @@ function cablecast_event_status( $show, $now = null ) { case 'upcoming': if ( ! empty( $show['scheduleStartTime'] ) ) { $start = strtotime( $show['scheduleStartTime'] ); - if ( false !== $start && ( $start - $now ) <= CABLECAST_STARTING_SOON_THRESHOLD ) { - return 'starting_soon'; + if ( false !== $start ) { + // "Starting soon" is a window around the scheduled start: within the + // threshold before it, or just after it. A start far in the past is + // not "soon", so leave it as plain "upcoming". + $until_start = $start - $now; + if ( $until_start <= CABLECAST_STARTING_SOON_THRESHOLD + && $until_start >= -CABLECAST_STARTING_SOON_THRESHOLD ) { + return 'starting_soon'; + } } } return 'upcoming'; @@ -102,15 +116,19 @@ function cablecast_live_events_shortcode() { return '

No live events scheduled right now.

'; } - // thumbnailUrl is a path relative to the Cablecast host. - $host = preg_replace( '#/cablecastapi$#', '', CABLECAST_API ); + // thumbnailUrl is a path relative to the Cablecast host. Strip whichever API base + // path is configured — /cablecastapi on self-hosted, /api on Reflect+. + $host = preg_replace( '#/(cablecastapi|api)$#', '', CABLECAST_API ); $out = '
    '; foreach ( array_merge( $live, $upcoming ) as $show ) { $is_live = ! empty( $show['isLive'] ) && cablecast_event_status( $show ) === 'live'; - $when = $is_live + // scheduleStartTime can be missing or unparseable; guard so a bad entry does not + // print the Unix epoch (or emit a notice) as the start time. + $start = ! empty( $show['scheduleStartTime'] ) ? strtotime( $show['scheduleStartTime'] ) : false; + $when = $is_live ? 'Live now' - : 'Starts ' . date_i18n( 'M j, g:i a', strtotime( $show['scheduleStartTime'] ) ); + : ( false !== $start ? 'Starts ' . date_i18n( 'M j, g:i a', $start ) : 'Upcoming' ); $out .= '
  • '; if ( ! empty( $show['thumbnailUrl'] ) ) { From 14d432fead5b83509419ddf002998cae01fa602b Mon Sep 17 00:00:00 2001 From: Ray Tiley Date: Mon, 24 Aug 2026 16:41:27 -0400 Subject: [PATCH 5/7] =?UTF-8?q?Address=20PR=20review=20(iteration=202)=20?= =?UTF-8?q?=E2=80=94=20WordPress=20liveness=20from=20status,=20not=20isLiv?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shortcode now tags each row live/not-live from the bucket cablecast_event_status sorted it into, instead of re-deriving it (and gating on isLive) in the render loop. showcaseEventStatus stays the single source of truth, and a live event renders live even if isLive is absent; also drops the redundant per-row status recompute. Co-Authored-By: Claude Opus 4.8 (1M context) --- showcase-events/wordpress.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/showcase-events/wordpress.php b/showcase-events/wordpress.php index 7f7dad2..66b9869 100644 --- a/showcase-events/wordpress.php +++ b/showcase-events/wordpress.php @@ -121,8 +121,19 @@ function cablecast_live_events_shortcode() { $host = preg_replace( '#/(cablecastapi|api)$#', '', CABLECAST_API ); $out = '
      '; - foreach ( array_merge( $live, $upcoming ) as $show ) { - $is_live = ! empty( $show['isLive'] ) && cablecast_event_status( $show ) === 'live'; + // Tag each row with its liveness from the bucket it was sorted into above, rather + // than re-deriving it (or second-guessing it against isLive) here. cablecast_event_status + // is the single source of truth, so a live event stays live even if isLive is absent. + $rows = array(); + foreach ( $live as $show ) { + $rows[] = array( $show, true ); + } + foreach ( $upcoming as $show ) { + $rows[] = array( $show, false ); + } + + foreach ( $rows as $row ) { + list( $show, $is_live ) = $row; // scheduleStartTime can be missing or unparseable; guard so a bad entry does not // print the Unix epoch (or emit a notice) as the start time. $start = ! empty( $show['scheduleStartTime'] ) ? strtotime( $show['scheduleStartTime'] ) : false; From f24016066a70e1e7c47aa46afacc343bab0caf6d Mon Sep 17 00:00:00 2001 From: Ray Tiley Date: Mon, 24 Aug 2026 16:48:03 -0400 Subject: [PATCH 6/7] =?UTF-8?q?Address=20PR=20review=20(iteration=203)=20?= =?UTF-8?q?=E2=80=94=20cache=20on=20failure,=20robust=20host,=20SRI=20note?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wordpress.php: cache the empty result for 15s on a failed/non-200 fetch so an API outage does not turn every page view into another call; rtrim the base URL before stripping so a configured trailing slash still resolves the host. - browser-player.html: note that the video.js CDN tags should be self-hosted or given Subresource Integrity for production use. Co-Authored-By: Claude Opus 4.8 (1M context) --- showcase-events/browser-player.html | 5 +++++ showcase-events/wordpress.php | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/showcase-events/browser-player.html b/showcase-events/browser-player.html index aa91c9c..e4c188c 100644 --- a/showcase-events/browser-player.html +++ b/showcase-events/browser-player.html @@ -21,6 +21,11 @@ Cablecast Live Events +