Skip to content

feat(infra): customisable embed, player events and a quality menu in Safari - #4190

Merged
richiemcilroy merged 26 commits into
mainfrom
feat/infra-embed-customisation
Nov 13, 2026
Merged

richiemcilroy merged 26 commits into
mainfrom
feat/infra-embed-customisation

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

Built on #4171 and #4187, both now on main; main (including #4189's snippet) is merged in.

What changes

The hosted embed (/dev/<playbackId>/embed) can now be styled and driven from the page that embeds it, and Safari on iPhone and iPad gets the same quality menu as every other browser.

Embed options, read strictly from the embed URL. A malformed value is ignored and never reaches the markup or the CSS.

Parameter Effect
color=3b82f6 Accent of the progress bar, volume and menu ticks (3 or 6 hex digits, # optional)
radius=12 Rounded corners, 0 to 24 px; the page behind the player turns transparent
quality=720 Highest rendition the player uses, in Auto and in the menu
quality_menu=0, speed_menu=0, pip=0, fullscreen=0 Hide those controls (fullscreen=0 also turns off double click and f)
poster=0 No poster
keyboard=0 No keyboard shortcuts

Each is one field of DeveloperVideoPlayback, read in developerVideoPlayback with the shared helpers.

The player shows no title, so there is no title parameter.

Player events over postMessage. The embed posts { type: "cap-player", event, currentTime, duration, paused, muted } to its parent for ready, play, pause, ended, timeupdate (at most about four a second), qualitychange (quality, auto) and error (code). It takes play, pause, seek (time), mute, unmute and setQuality (quality) only from its parent window, and only in validated shapes. A play, seek or setQuality sent on ready, before the source, metadata or renditions are ready, is held and applied when they are. Events carry no URLs. An embed with nothing to play (taken down, failed, or a paused app) shows its usual neutral page whatever the parameters and sends error with code: "unavailable" or "paused".

Quality menu on native HLS. iPhone and iPad play HLS natively, which offers no way to pick a rendition. The player now reads the renditions from the master playlist once metadata is in and shows the usual menu. A pick loads master.m3u8?rendition=720p, a master that lists only that rendition, keeping the time and play state; Auto goes back to the full master. A rendition's own playlist could not be used: the audio is a separate rendition, so it would play silent. The playback route serves the narrowed master (and ?max=720 for the quality cap); an unknown or malformed value leaves the master as it was. Renditions are capped, picked and ordered by height, never by bandwidth: the ladder is constant quality, so a taller rendition can need less (tested with such a ladder). The cap wins over a pick.

Console. The embed code on a video's page has toggles for accent colour, controls, autoplay muted, captions (when the video has them) and the quality menu, and the snippet updates as they change. Only the src changes; the iframe's attributes stay as #4189 sets them.

Docs. "Embed Options" and "Player Events" sections next to Embed Parameters in the REST API docs, with an example of each.

What does not change

  • Regular Cap share pages (/s/...) and their embed do not use any of these files. CapPlayer is used only by the Infra dev pages, the Infra console and the Infra landing film; the new props default to the current behaviour.
  • With no new parameters, the embed's markup is identical to before (tested).

Proof

Two test videos uploaded to a test app (1080p with sound, and a silent one for the iOS Simulator), embedded cross-origin from a local page on a Mac. The embed page was the real DeveloperVideoView bundled from this branch, with playback proxied to the live playback URLs through this branch's playlist code. Deleted afterwards.

Browser What was proven Result
Chrome 157 Every parameter, every event and command, hostile commands and URL values, commands sent on ready, quality menu and cap, taken-down embed; 342x192 touch with captions (44 px targets, caption lift); 280/320/400 px bars, mouse and touch 34/34, narrow 12/12
Edge 156 Same, without the phone-size touch check 33/33, narrow 12/12
Firefox 151 Same (menu click with the mouse included) 33/33, narrow 6/6
WebKit (Playwright) Same 33/33, narrow 12/12
Safari 27 (safaridriver) Events, commands, hls.js menu; native HLS (MediaSource removed): menu, pick while playing and paused, two quick picks, Auto; custom look 16/16
iOS Simulator, iPhone 17 Pro, Safari Native HLS (no MediaSource): menu lists Auto + every rendition, pick 360p keeps time and play state, Auto restores; events and setQuality from a cross-origin page pass

Screenshots: a customised embed (orange accent, 16 px corners, speed, picture in picture and full screen hidden) in Chrome, Edge, Firefox, WebKit and Safari; the quality menu in Chrome, Firefox, Safari (hls.js) and Safari native HLS; the native menu and 360p on iPhone; a 342 px phone column; a 280 px bar.

Bytes before play. The player component adds about 1.56 KB gzip over main (esbuild, full minify). Measured earlier on real Next builds of /infra, which loads the same player: the player chunk grew 1,301 B and global CSS 54 B, every other chunk byte-identical.

Tests

  • Parameter parsing, including hostile colours, radii and quality values, and that none of them reach the page.
  • The playback route's rendition pick and cap, including malformed and repeated values and a ladder whose bandwidth does not rise with height.
  • Native quality switching: listing, switching while playing and paused, Auto, unknown picks, a failed master read, detach.
  • The events API: command validation (hostile shapes), parent-only commands, the timeupdate throttle, refused play, commands held until metadata.
  • A taken down embed is the same page whatever the parameters, and sends error.
  • The console snippet builder, including hostile colours.

?rendition=720p serves a master that lists only that rendition, with its
audio, so Safari's own HLS player can be switched to it. ?max=720 keeps
renditions no taller than 720. Malformed or unknown values leave the
master as it is.
The player reads the renditions from the master playlist once metadata is
in, and a pick loads the single-rendition master, keeping the time and
play state. Auto goes back to the full master.
Events ready, play, pause, ended, timeupdate (about four a second),
qualitychange and error go to the embedding page. Commands play, pause,
seek, mute, unmute and setQuality are taken only from that page's window
and only in validated shapes. Adds keyboard and full screen switches.
… buttons

color, radius, quality, quality_menu, speed_menu, pip, fullscreen, poster
and keyboard are read strictly from the embed URL; anything malformed is
ignored and never reaches the markup.
Accent colour, controls, autoplay muted, captions and the quality menu
update the snippet as they change.
@richiemcilroy
richiemcilroy force-pushed the feat/infra-embed-customisation branch from 0ab5570 to 27784a0 Compare November 13, 2026 08:02
@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 08:02 Inactive
@vercel

vercel Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
cap-status Skipped Skipped Nov 13, 2026 10:36am UTC

@richiemcilroy
richiemcilroy marked this pull request as ready for review November 13, 2026 08:03
@sandchest-com

sandchest-com Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Sandchest

Sandchest reviewed 27f7493: nothing exploitable found in this change.

Sandchest reviews every change for vulnerabilities an attacker could use · 6 credits · Review details

@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 08:05 Inactive

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found 1 potential issue (1 medium).

Open in Capy

@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 08:12 Inactive

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found 1 potential issue (1 medium).

Open in Capy

…ight on an uneven ladder

A constant quality ladder can give a taller rendition less bandwidth, so
the cap, the fallback and the menu order are tested against one.
The console options change only the embed URL. The new docs examples use
allow="autoplay" with allowfullscreen.
@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 08:16 Inactive
quality_menu, speed_menu, pip and fullscreen are read with flag() like the
other switches; the page turns them into the player's hidden controls.
@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai review

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 10:21 Inactive
@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai review

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

View 1 other finding in Capy.

Open in Capy

@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 10:26 Inactive
@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai review

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

View 1 other finding in Capy.

Open in Capy

@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 10:31 Inactive
@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai review

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

View 3 other findings in Capy.

Open in Capy

@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 10:36 Inactive

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

View 1 other finding in Capy.

Open in Capy

@richiemcilroy
richiemcilroy merged commit 79615c1 into main Nov 13, 2026
39 checks passed

This branch was successfully deployed

1 active and 1 inactive deployments
Preview – cap-web — 27f74938 Deployed Nov 13, 2026 by vercel[bot]
Preview – cap-status — 27f74938 Deployed Nov 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant