-
Notifications
You must be signed in to change notification settings - Fork 1
Add showcase-events examples (publicsitedata) #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
raytiley
wants to merge
7
commits into
main
Choose a base branch
from
showcase-events-examples
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ab1850d
Add showcase-events examples built on publicsitedata
raytiley a971c34
showcase-events: read the server-derived showcaseEventStatus field
raytiley 70dbff3
showcase-events: drop the unused derive fallback; document canceled/e…
raytiley a38c616
Address PR review — harden WordPress + browser examples
raytiley 14d432f
Address PR review (iteration 2) — WordPress liveness from status, not…
raytiley f240160
Address PR review (iteration 3) — cache on failure, robust host, SRI …
raytiley 048f5be
Address PR review (iteration 4) — normalize base URL in fetch too
raytiley File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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"; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.