Skip to content

feat: add title display and media metadata state#1603

Open
Jerricho93 wants to merge 13 commits into
videojs:mainfrom
Jerricho93:pr/title-core-default
Open

feat: add title display and media metadata state#1603
Jerricho93 wants to merge 13 commits into
videojs:mainfrom
Jerricho93:pr/title-core-default

Conversation

@Jerricho93

@Jerricho93 Jerricho93 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add metadataFeature to packages/core with title state and setTitle — syncs to navigator.mediaSession.metadata when available
  • Add TitleCore, TitleDataAttrs, and selectMetadata selector for runtime-agnostic title logic
  • Add <media-title> custom element (HTML) and Title.Value component (React) that read from the metadata store
  • Add media-title to default skin templates (HTML and React, CSS and Tailwind variants) — fades in when the player is paused
  • Default skin overlay gains a conditional top gradient that activates only when a title is visible (paused + title present)
  • Sandbox derives title from source label; accepts media-title attribute / title prop on all player elements — never uses the native title= attribute, avoiding the browser tooltip issue (Bug: Title Hover Issue #791)

Minimal skin title display is scoped to a follow-up PR.

Test plan

  • pnpm -F @videojs/core test src/core/ui/title
  • pnpm -F @videojs/html test src/ui/title
  • pnpm -F @videojs/react test src/ui/title
  • pnpm typecheck
  • pnpm lint
  • Sandbox: set media-title on HTML player → title fades in when paused, hidden on error, no native browser tooltip on hover
  • Sandbox: React skin with title prop → same behavior
  • Verify navigator.mediaSession.metadata.title is set in Chrome
  • pnpm check:workspace

Closes #1123


Note

Medium Risk
Touches shared player feature presets and MediaSession behavior across HTML and React entry points, but changes are additive with dedicated tests and limited to metadata/UI wiring.

Overview
Introduces metadata as a first-class player store feature: title plus setTitle, included in audio/video/live presets, with selectMetadata and tests that verify navigator.mediaSession.metadata stays in sync (and clears on detach/destroy).

Shared title presentation logic lands as TitleCore / data attrs, backed by <media-title> (HTML) and Title.Value (React). Default video/live skins now render that overlay and tweak the overlay gradient so the top scrim appears when a title is shown on pause.

Integrators set titles via media-title on player elements (not native title) or the React Provider title prop; sandboxes derive titles from source labels through renderPlayerAttrs / useTitle.

Reviewed by Cursor Bugbot for commit d5d413c. Bugbot is set up for automated code reviews on this repo. Configure here.

Introduces MetadataFeature with title state, TitleCore/TitleElement
(HTML), Title.Value (React), and default skin styling. Includes:
- metadataFeature with setTitle and MediaSession sync
- <media-title> / Title.Value components reading from store
- Play button aria-label enriched with title when set
- Default skin overlay/title CSS and Tailwind variants
- Sandbox sources wired with title; site API reference page

Closes videojs#1123

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented May 26, 2026

Copy link
Copy Markdown

@Jerricho93 is attempting to deploy a commit to the Mux Team on Vercel.

A member of the Team first needs to authorize it.

@netlify

netlify Bot commented May 26, 2026

Copy link
Copy Markdown

👷 Deploy request for vjs10-site pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit d5d413c

Jerricho93 and others added 2 commits May 26, 2026 16:15
Omit 'children' from Title.Value's ValueProps so consumers can't
accidentally pass children that would be silently overridden.
Add PlayButtonCore test coverage for the setMetadata/title-in-label
behavior introduced in the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Jerricho93 Jerricho93 marked this pull request as ready for review May 26, 2026 20:11
Comment thread packages/html/src/define/video/player.ts
Jerricho93 and others added 3 commits May 26, 2026 17:27
store.detach() resets all feature state to initial values, including
title → null. The previous implementation only called setTitle when
the mediaTitle property changed on the element (HTML) or when title/store
deps changed (React), so a static title would not be restored after a
detach/reattach cycle.

HTML: remove the changed.has('mediaTitle') guard so update() always
syncs the current mediaTitle value on every render cycle.

React: capture title in a ref and re-apply it immediately after
store.attach() so the title is restored without adding it to the
attach effect's dependency array (which would cause extra reattaches
on every title change).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Title should only appear in the visual overlay, not in button labels.
Reverts PlayButtonCore.setMetadata(), the metadata selector wiring in
PlayButtonElement, and the createMediaButton expansion in PlayButton.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add useTitle() hook that derives the title from the selected source,
and pass it as the title prop to Provider in all React video/audio
templates so the title overlay renders correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/core/src/dom/store/features/metadata.ts
Jerricho93 and others added 3 commits May 26, 2026 18:11
When the store detaches, signals are aborted before state resets, so
the sync subscription was already removed when title resets to null —
leaving the previous title visible in the OS lock-screen controls.

Fix: capture navigator.mediaSession at attach time and clear it
explicitly in the abort handler. Also adds a metadataFeature test
suite covering MediaSession sync, detach, destroy, and reattach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…overlay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/core/src/dom/store/features/metadata.ts
@vercel

vercel Bot commented May 27, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Jun 4, 2026 2:13pm

Request Review

@sampotts sampotts left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looking good! Cheers for doing this. We probably want Rahim or Wes to give a once over too but I've left a few comments.

Also, I built and tried the sandbox but couldn't see the title:
https://v10-sandbox-git-fork-jerricho93-pr-title-core-default-mux.vercel.app/?platform=react&styling=css&preset=hls-video&skin=default&source=hls-4&autoplay=0&muted=0&loop=0&preload=metadata

Comment thread apps/sandbox/app/shared/sources.ts Outdated
export const SOURCES = {
'hls-1': {
label: 'HLS - Big Buck Bunny',
title: 'Big Buck Bunny',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A nit and not a blocker but we could probably derive the label from {this.type.toUpperCase()} {this.title} to save the double up.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Or inversely, just do label.split(' - ')[1] to get the title.

if (!media) return;
return store.attach({ media, container });
const cleanup = store.attach({ media, container });
const s = store.state as { setTitle?: (t: string | null) => void };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: can we use state here instead of s? It's a personal thing but I'm not a fan of the single letter variable names (e.g. e for error or event) unless it's inside a simple .map() or .filter() since it makes it a bit harder to understand.

Comment on lines +10 to +11
linear-gradient(to bottom, oklch(0 0 0 / 0.5), oklch(0 0 0 / 0.3) 25%, oklch(0 0 0 / 0)),
linear-gradient(to top, oklch(0 0 0 / 0.5), oklch(0 0 0 / 0.3) 25%, oklch(0 0 0 / 0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Couldn't we combine these and just add another stop in? Or we could just go with oklch(0 0 0 / 0.5), oklch(0 0 0 / 0), oklch(0 0 0 / 0.5)

Also, how do we handle if there's no title/metadata? It might look a little weird to have the extra gradient if there's no title/metadata.

// Default: hidden
'opacity-0',
'bg-linear-to-t from-black/50 via-black/30 via-25% to-transparent',
'[background-image:linear-gradient(to_top,oklch(0_0_0_/_0.5),oklch(0_0_0_/_0.3)_25%,oklch(0_0_0_/_0)),linear-gradient(to_bottom,oklch(0_0_0_/_0.5),oklch(0_0_0_/_0.3)_25%,oklch(0_0_0_/_0))]',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If we simplify as above, we can revert to using the native Tailwind classnames.

text-overflow: ellipsis;
font-size: 1rem;
font-weight: 500;
color: var(--media-color-primary, oklch(1 0 0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did you try without this? I think the currentColor would be --media-color-primary but I could be wrong!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

.media-title and .media-controls are siblings, so we can't inherit from that. What could be done is defining color in .media-default-skin--video and then both components can inherit from that. Would that be a better solution?

'py-3 px-4',
'pointer-events-none',
'text-base font-medium',
'[color:var(--media-color-primary,oklch(1_0_0))]',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

As above re: whether we need this.

// Default: hidden
'opacity-0',
'bg-linear-to-t from-black/70 via-black/50 via-[7.5rem] to-transparent',
'[background-image:linear-gradient(to_top,oklch(0_0_0_/_0.5),oklch(0_0_0_/_0.3)_25%,oklch(0_0_0_/_0)),linear-gradient(to_bottom,oklch(0_0_0_/_0.5),oklch(0_0_0_/_0.3)_25%,oklch(0_0_0_/_0))]',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

As with the other skin but I guess this crept in?

- Default skin overlay: bottom gradient always visible; top darkening
  added only when a title is present via :has([data-has-title]).
  CSS and Tailwind aligned (via-black/30 at 25%).
- Minimal skin Tailwind overlay reverted to main — changes belong in
  pr/title-minimal.
- Sandbox: title derived from label instead of a redundant title field;
  remove title field from all sources.
- create-player: rename s → state for readability (review fix).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/skins/src/default/css/components/title.css
All selectors in title.css were missing the .media-default-skin
ancestor prefix, causing styles to apply globally across all skins.
Aligned with the convention used by every other component CSS file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sampotts

sampotts commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

It's looking good. I think we'll want some responsive styles for the font size and padding. On a smaller screen let's go with:

font-size: 0.875rem;
padding: 1rem 1.5rem;

then using an container query for @container media-root (width > 42rem) to pump up the font size and padding:

font-size: 1rem;
padding: 1.5rem 2rem;

I'd also like to add a text-shadow to the title to improve the contrast a touch. We have a shadow color based on the current color that we should be able to use here. It might need the color definition moved to the skin though as you suggested. We'd just need to make sure that --media-color-primary still works.

I also reckon the title should only show when the player is paused rather than every time the controls are visible? What do you think?

@Jerricho93

Copy link
Copy Markdown
Collaborator Author

@sampotts sure, I can push those changes. One question regarding displaying the title: if we're going to just display it when the video is paused, should the top gradient also be displayed only when paused? Otherwise it may cause the player to look too "dark" on the top side for no specific reason. What do you think?

Also, we held off on moving --media-color-primary into the skin. If we define it on .media-default-skin--video, the class selector would win over a consumer's :root override due to specificity, and we would be then ignoring their customization. This pattern keeps the variable "unowned" by the skin so the cascade stays clean. Does that make sense, or were you thinking of a different approach?

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b1a52ec. Configure here.

Comment thread packages/html/src/define/video/player.ts
Comment thread packages/core/src/core/media/state.ts Outdated
…tle change

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Feature: Title Display

2 participants