Skip to content

feat(video-player): migrate from react player to media-chrome#799

Merged
rogerantony-dev merged 24 commits intodevfrom
feat/media-chrome-video-player
Feb 27, 2026
Merged

feat(video-player): migrate from react player to media-chrome#799
rogerantony-dev merged 24 commits intodevfrom
feat/media-chrome-video-player

Conversation

@rogerantony-dev
Copy link
Contributor

@rogerantony-dev rogerantony-dev commented Feb 23, 2026

PR Checklist

  • Addresses an existing open issue: fixes #000
  • Steps in CONTRIBUTING.md were taken

Overview

Summary by CodeRabbit

  • New Features

    • Redesigned video player UI with a full control bar: play/pause, volume/mute (hover volume popup), seek/preview thumbnails, playback-rate menu, captions, picture-in-picture (when available), and fullscreen
    • Optional YouTube playback integration
  • Improvements

    • Time display now shows duration
    • Per-video error handling improved
    • New icon set, comprehensive visual theme, animations, and enhanced keyboard/interaction behaviors

@vercel
Copy link

vercel bot commented Feb 23, 2026

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

Project Deployment Actions Updated (UTC)
recollect Ready Ready Preview Feb 27, 2026 10:28am

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces the ReactPlayer-based implementation with a media-chrome-based VideoPlayer (native video and youtube-video), adds MediaProvider/MediaController-based controls, new icons and theme CSS, TypeScript JSX typing for youtube-video, and updates dependencies (media-chrome added, react-player removed, youtube-video-element added).

Changes

Cohort / File(s) Summary
Dependency Updates
package.json
Removed react-player; added media-chrome (4.18.0) and youtube-video-element (1.9.0).
Core Video Player
src/components/VideoPlayer.tsx
Reimplemented player using MediaProvider/media-chrome controls and a VideoPlayerInner; supports youtube-video or native video; adds VideoPlayerProps (isYouTube?: boolean, onError?: () => void, src: string); dynamic youtube-element import; native error listener wiring; expanded control surface (play, mute/volume, time, seek, captions/preview, playback-rate menu, PiP (non-YouTube), fullscreen).
Lightbox Integration
src/components/lightbox/LightboxRenderers.tsx, src/components/lightbox/LightBox.tsx
Removed isActive prop from VideoSlide and YouTubeSlide call sites; YouTubeSlide layout adjusted; VideoPlayer is now passed isYouTube where applicable and no longer receives isActive.
Icons
src/components/video-player-icons.tsx
Added multiple SVG icon components: PlayPauseIcon, MuteIcon, SettingsIcon, PipIcon, FullscreenIcon and internal helpers.
Styles & Theme
src/components/video-player-theme.css, src/components/video-player-theme.ts
Added comprehensive CSS theme and animations for media-chrome controller; introduced BASE_CONTROLLER_STYLE, exported CONTROLLER_STYLE, new YOUTUBE_CONTROLLER_STYLE, and CONTROL_BAR_STYLE.
Types
src/types/youtube-video-element.d.ts
Added JSX intrinsic element typing for custom <youtube-video> mapped to standard video props.
Misc
.cspell/project-words.txt
Added spell-check entries: breakpointmd, mediaisfullscreen, notooltip.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as "MediaController / ControlBar"
    participant Provider as "MediaProvider"
    participant Player as "youtube-video / video"
    participant Handler as "ErrorHandler"

    User->>UI: Play / Pause / Seek / Volume / PiP / Fullscreen
    UI->>Provider: dispatch control action
    Provider->>Player: apply command (play/pause/seek/volume)
    Player->>Provider: emit media state events
    Provider-->>UI: update control surface
    Player->>Handler: native error event
    Handler->>Handler: invoke onError callback
    Handler-->>UI: surface error state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • abhishekmg
  • navin-moorthy

Poem

🐰 I hopped in with chrome and gleam,
Controls that click and softly stream,
I fetch YouTube when called to play,
Icons, themes, and CSS array,
Hooray — the pixels dance and beam! 🎬

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: migrating video player implementation from react-player to media-chrome.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/media-chrome-video-player

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (2)
src/components/video-player-icons.tsx (1)

65-81: Define a proper interface for SettingsIcon props.

Per coding guidelines, component props should use a named TypeScript interface. The current inline optional parameter (props?: { slotIcon?: boolean }) works but is less discoverable.

♻️ Suggested refactor
+interface SettingsIconProps {
+	slotIcon?: boolean;
+}
+
-export function SettingsIcon(props?: { slotIcon?: boolean }) {
+export function SettingsIcon(props?: SettingsIconProps) {

As per coding guidelines, "Always define TypeScript interfaces for React component props."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player-icons.tsx` around lines 65 - 81, The SettingsIcon
component currently uses an inline type for props; create a named TypeScript
interface (e.g., SettingsIconProps) that declares slotIcon?: boolean and update
the SettingsIcon signature to accept that interface (use props:
SettingsIconProps or props?: SettingsIconProps as appropriate), leaving the
existing usage of props?.slotIcon and the Icon child unchanged so slot attribute
handling stays the same.
src/components/video-player-theme.ts (1)

3-14: Potential token duplication between JS styles and CSS theme file.

CONTROLLER_STYLE sets --base: 20px and color tokens here, while video-player-theme.css independently declares --video-base: 20px and similar color values on media-controller. If one is updated without the other, they'll drift. Consider whether the CSS file alone can handle theming (via the style attribute on the element) or consolidate tokens in one place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player-theme.ts` around lines 3 - 14, CONTROLLER_STYLE
duplicates CSS tokens declared in video-player-theme.css (e.g., "--base" vs
"--video-base" on media-controller), so consolidate to a single source of truth:
either remove the duplicate tokens from CONTROLLER_STYLE and apply the CSS theme
only (use the element's style attribute to override CSS variables when dynamic
values are needed), or keep CONTROLLER_STYLE as the canonical token source and
update video-player-theme.css to reference the same variable names (e.g., align
"--base" and color variables) so both use identical variable names; update
references in code that use CONTROLLER_STYLE and the CSS selector
media-controller to the chosen variable names and remove the redundant
definitions to prevent drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Line 121: Update the media-chrome dependency entry in package.json from
"media-chrome": "4.17.2" to "media-chrome": "4.18.0"; after changing the version
run your package manager to regenerate the lockfile (npm install or yarn
install) and run the test/build to verify there are no regressions, and commit
the updated package.json and lockfile changes.

In `@src/components/video-player-theme.css`:
- Line 227: Add the media-chrome attribute names that CSpell flags as unknown to
the project dictionary: add "breakpointmd" and "mediaisfullscreen" (and any
other flagged attribute tokens such as those shown in the review) to
.cspell/project-words.txt or run the repo helper command (pnpm fix:spelling) so
the CSS selectors like media-controller[breakpointmd] and
media-controller[mediaisfullscreen] are accepted; update the dictionary with
exact token strings to avoid future warnings.
- Around line 311-325: The CSS block for selector media-controller
.settings-menu-wrap media-playback-rate-menu has Stylelint
declaration-empty-line-before errors due to extra blank lines before
declarations and uses multiple !important overrides; remove the stray empty
lines so no blank line precedes declarations (e.g., before position, right,
left, transform, padding-block, border-radius, backdrop-filter) and either avoid
or minimize !important on position/right/left/transform by using a more specific
selector or reordering rules; if !important is truly required to override
media-chrome internals, add a short comment above the four !important
declarations explaining why they’re necessary (e.g., to override media-chrome
inline/author styles) so reviewers understand the intent.
- Around line 182-193: Remove the blank lines inside the CSS rule block that
contains the declarations starting with transform-origin and transform (the
block ending with the closing brace shown), so there are no empty lines before
subsequent declarations like width, height, backdrop-filter, transition, and the
`@apply` line; collapse those declarations into consecutive lines to satisfy
Stylelint's declaration-empty-line-before rule and allow pnpm fix:css to pass.

In `@src/components/VideoPlayer.tsx`:
- Line 91: Add the missing media-chrome attribute token "notooltip" to the
CSpell dictionary so the spellchecker stops flagging it; update
.cspell/project-words.txt by adding the word notooltip (this relates to the
MediaMuteButton usage in src/components/VideoPlayer.tsx where ref sets the
"notooltip" attribute).
- Around line 35-40: VideoPlayerProps includes isActive but the components never
use it, so media keeps playing when a slide becomes inactive; update VideoPlayer
and/or VideoPlayerInner to watch isActive (via useEffect) and control playback:
get the video element ref used in the component (the same ref passed to the
media element), and when isActive becomes true call el.play() (handle the
Promise) and when false call el.pause(); for embedded YouTube (isYouTube)
integrate with the player API or pause via postMessage/iframe API, and call
onError if play() rejects; ensure you guard null refs and do not block UI while
awaiting play().
- Around line 76-77: The youtube-video branch in VideoPlayer.tsx is missing
caption support; inspect whether the youtube-video-element supports <track>
children and, if it does, add a <track> element (kind="captions"/"subtitles",
srclang, src, default as appropriate) as a child of the <youtube-video ref={ref}
slot="media" src={src}> element to mirror the native <video> branch; if the
package does not support <track>, wire up captions via the YouTube player
API/params (enablecc=1 & track selection) or render an accessible fallback (a
visually hidden transcript/CC toggle and proper aria-label/aria-describedby) so
that VideoPlayer still provides captioning for audio/video content.

---

Nitpick comments:
In `@src/components/video-player-icons.tsx`:
- Around line 65-81: The SettingsIcon component currently uses an inline type
for props; create a named TypeScript interface (e.g., SettingsIconProps) that
declares slotIcon?: boolean and update the SettingsIcon signature to accept that
interface (use props: SettingsIconProps or props?: SettingsIconProps as
appropriate), leaving the existing usage of props?.slotIcon and the Icon child
unchanged so slot attribute handling stays the same.

In `@src/components/video-player-theme.ts`:
- Around line 3-14: CONTROLLER_STYLE duplicates CSS tokens declared in
video-player-theme.css (e.g., "--base" vs "--video-base" on media-controller),
so consolidate to a single source of truth: either remove the duplicate tokens
from CONTROLLER_STYLE and apply the CSS theme only (use the element's style
attribute to override CSS variables when dynamic values are needed), or keep
CONTROLLER_STYLE as the canonical token source and update video-player-theme.css
to reference the same variable names (e.g., align "--base" and color variables)
so both use identical variable names; update references in code that use
CONTROLLER_STYLE and the CSS selector media-controller to the chosen variable
names and remove the redundant definitions to prevent drift.

package.json Outdated
"jwt-decode": "4.0.0",
"lodash": "4.17.23",
"match-sorter": "8.2.0",
"media-chrome": "4.17.2",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check latest versions on npm
echo "=== media-chrome latest version ==="
curl -s https://registry.npmjs.org/media-chrome/latest | jq '.version'

echo ""
echo "=== youtube-video-element latest version ==="
curl -s https://registry.npmjs.org/youtube-video-element/latest | jq '.version'

echo ""
echo "=== media-chrome package info ==="
curl -s https://registry.npmjs.org/media-chrome/4.17.2 | jq '{version: .version, published: .time["4.17.2"]}'

echo ""
echo "=== youtube-video-element package info ==="
curl -s https://registry.npmjs.org/youtube-video-element/1.9.0 | jq '{version: .version, published: .time["1.9.0"]}'

Repository: timelessco/recollect

Length of output: 334


🏁 Script executed:

#!/bin/bash
# Check for vulnerabilities using npm audit for the specific versions
echo "=== Checking media-chrome 4.17.2 ==="
npm view media-chrome@4.17.2 --json | jq '{version, homepage, license, _lastPublishedTime}'

echo ""
echo "=== Checking youtube-video-element 1.9.0 ==="
npm view youtube-video-element@1.9.0 --json | jq '{version, homepage, license, _lastPublishedTime}'

echo ""
echo "=== media-chrome changelog (4.17.2 to 4.18.0) ==="
npm view media-chrome versions --json | jq '.[-3:]'

Repository: timelessco/recollect

Length of output: 1220


🌐 Web query:

media-chrome 4.18.0 release notes changelog

💡 Result:

media-chrome 4.18.0 — changelog (released 2026-02-19)

Features

  • [a11y + i18n] Translate time phrases (PR #1232) [1]

Bug fixes

  • i18n aria-label fixes for time range and seek buttons (PR #1253) [1]
  • Firefox accessibility test fix (PR #1258) [1]
  • Patch memory leaks (PR #1261) [1]
  • Thumbnail preview sizing fix (PR #1262), closes #1160 [1]

Upstream compare link: v4.17.2...v4.18.0 [1]


Update media-chrome to 4.18.0. The current version 4.17.2 is outdated. The latest stable release includes memory leak patches, accessibility improvements, and bug fixes. youtube-video-element@1.9.0 is current.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 121, Update the media-chrome dependency entry in
package.json from "media-chrome": "4.17.2" to "media-chrome": "4.18.0"; after
changing the version run your package manager to regenerate the lockfile (npm
install or yarn install) and run the test/build to verify there are no
regressions, and commit the updated package.json and lockfile changes.

Comment on lines +35 to +40
export interface VideoPlayerProps {
isActive: boolean;
src: string;
isYouTube?: boolean;
onError?: () => void;
}) => {
const playerRef = useRef<HTMLVideoElement | null>(null);
src: string;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

isActive prop is accepted but never used — video won't pause when navigating away.

VideoPlayerProps declares isActive and callers pass it (e.g., LightboxRenderers.tsx passes isActive={isActive ?? false}), but it's never read in VideoPlayerInner or VideoPlayer. With the previous react-player implementation, isActive likely controlled playing state. Without it, video playback continues when the user navigates to a different lightbox slide.

You need to wire isActive to pause/play the media element — for example, via useEffect calling el.play()/el.pause() on the video ref, or using media-chrome's store API.

Also applies to: 138-144

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 35 - 40, VideoPlayerProps
includes isActive but the components never use it, so media keeps playing when a
slide becomes inactive; update VideoPlayer and/or VideoPlayerInner to watch
isActive (via useEffect) and control playback: get the video element ref used in
the component (the same ref passed to the media element), and when isActive
becomes true call el.play() (handle the Promise) and when false call el.pause();
for embedded YouTube (isYouTube) integrate with the player API or pause via
postMessage/iframe API, and call onError if play() rejects; ensure you guard
null refs and do not block UI while awaiting play().

Comment on lines +76 to +77
{isYouTube ? (
<youtube-video ref={ref} slot="media" src={src} />
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing <track> element on <youtube-video> for accessibility.

The native <video> branch (line 79–81) includes a <track> for captions, but the <youtube-video> branch does not. The coding guidelines require caption tracks for all audio and video elements.

Note that youtube-video-element may not support <track> children the same way native video does — verify whether captions can be provided via YouTube's built-in CC or if a <track> child is supported.

As per coding guidelines, "Always include caption tracks for audio and video elements."

Does youtube-video-element npm package support track element for captions?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 76 - 77, The youtube-video
branch in VideoPlayer.tsx is missing caption support; inspect whether the
youtube-video-element supports <track> children and, if it does, add a <track>
element (kind="captions"/"subtitles", srclang, src, default as appropriate) as a
child of the <youtube-video ref={ref} slot="media" src={src}> element to mirror
the native <video> branch; if the package does not support <track>, wire up
captions via the YouTube player API/params (enablecc=1 & track selection) or
render an accessible fallback (a visually hidden transcript/CC toggle and proper
aria-label/aria-describedby) so that VideoPlayer still provides captioning for
audio/video content.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (6)
src/components/video-player-theme.css (3)

315-329: ⚠️ Potential issue | 🟡 Minor

Stylelint declaration-empty-line-before errors on lines 325 and 328 — remove stray blank lines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player-theme.css` around lines 315 - 329, The CSS block
for selector "media-controller .settings-menu-wrap media-playback-rate-menu"
contains stray blank lines that trigger stylelint's
declaration-empty-line-before rule; remove the empty lines between declarations
(e.g., between "--media-control-padding: 0;" and the comment, and between
"transform: translateX(-50%) !important;" and "padding-block: 0;") so all
declarations are directly adjacent per stylelint expectations, then reformat to
preserve spacing only where allowed by the rule.

186-197: ⚠️ Potential issue | 🟡 Minor

Stylelint declaration-empty-line-before errors on lines 189, 192, 194 — remove stray blank lines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player-theme.css` around lines 186 - 197, Remove the
stray empty lines inside the CSS declaration block containing transform-origin /
transform / width / height / backdrop-filter / transition / `@apply` (the rule
that sets transform-origin: center and transform: translateY(...)). Ensure there
are no blank lines immediately before the declarations on lines with transform,
width, and backdrop-filter so the declaration-empty-line-before lint rule is
satisfied; collapse those blank lines so all property declarations are
contiguous within that selector block.

231-231: ⚠️ Potential issue | 🟡 Minor

CSpell pipeline failures for breakpointmd and mediaisfullscreen — add to project dictionary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player-theme.css` at line 231, The CSpell pipeline is
flagging custom attribute tokens like "breakpointmd" and "mediaisfullscreen"
used in selectors such as media-controller[breakpointmd] and the
mediaisfullscreen attribute; update the project's cspell dictionary (e.g., add
these tokens to the words array in your cspell.json or dictionary file) to
whitelist breakpointmd and mediaisfullscreen so the CSS selectors
(media-controller[breakpointmd], media-time-display:not([showduration]), and any
media-controller[mediaisfullscreen]) no longer trigger linting failures.
src/components/VideoPlayer.tsx (3)

35-40: ⚠️ Potential issue | 🔴 Critical

isActive is declared in VideoPlayerProps but never consumed — video won't pause on slide change.

VideoPlayerInner explicitly drops isActive via Omit<VideoPlayerProps, "isActive">, so playback is never paused when the containing lightbox slide becomes inactive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 35 - 40, The prop isActive on
VideoPlayerProps is declared but dropped by the Omit in VideoPlayerInner, so the
component never pauses playback on slide change; restore and use isActive by
including it in VideoPlayerInner's prop type (remove Omit<..., "isActive"> or
add isActive to the inner props) and implement a useEffect inside
VideoPlayerInner (or VideoPlayer) that watches isActive and calls the video
element's pause() (via ref) when isActive becomes false and resumes or does
nothing when true; reference VideoPlayerProps, VideoPlayerInner, isActive and
the video element ref to locate where to add this logic.

91-91: ⚠️ Potential issue | 🟡 Minor

CSpell pipeline failure: add notooltip to the project dictionary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` at line 91, The spellchecker flags the
attribute value "notooltip" used on MediaMuteButton; add "notooltip" to the
project's cspell dictionary (the words array in your cspell configuration) so
the attribute is recognized, then commit the updated cspell config; reference
the MediaMuteButton usage to locate the offending token.

76-82: ⚠️ Potential issue | 🟡 Minor

Missing <track> on <youtube-video> — caption accessibility requirement not met for YouTube path.

The native <video> branch includes a <track> element, but the <youtube-video> branch does not. As per coding guidelines, caption tracks are required for all audio and video elements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 76 - 82, The YouTube branch is
missing a captions track: update the JSX in VideoPlayer (the isYouTube branch
rendering <youtube-video ... />) to include a <track> element matching the
native <video> branch (e.g., default kind="captions" label and srcLang
attributes) so captions exist for both paths; keep existing props like ref,
slot="media", src and crossOrigin on the <youtube-video> and add the <track> as
a child to satisfy the caption accessibility requirement.
🧹 Nitpick comments (2)
src/components/video-player-theme.css (1)

203-213: .vol-wrap:focus is a dead selector — <div class="vol-wrap"> is not focusable.

Line 208 lists media-controller .vol-wrap:focus as a condition to show the volume popup. A plain <div> without tabIndex cannot receive focus directly, so :focus never matches. Only :hover and :focus-within (line 209) are ever active. The :focus-within selector already covers keyboard navigation into the MediaVolumeRange inside. Remove line 208 to avoid dead code.

♻️ Proposed fix
 media-controller .vol-wrap:hover,
-media-controller .vol-wrap:focus,
 media-controller .vol-wrap:focus-within,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player-theme.css` around lines 203 - 213, The CSS
includes a dead selector "media-controller .vol-wrap:focus" because .vol-wrap is
a non-focusable div; remove that selector from the rule set so only effective
selectors remain (e.g., keep :hover and :focus-within). Update the rule block
that currently lists "media-controller .vol-wrap:hover, media-controller
.vol-wrap:focus, media-controller .vol-wrap:focus-within, ..." to drop the
".vol-wrap:focus" entry, ensuring the volume popup visibility is controlled by
:hover and :focus-within (which covers keyboard focus into the
MediaVolumeRange).
src/components/video-player-theme.ts (1)

16-23: alignItems and gap in CONTROL_BAR_STYLE duplicate the CSS @apply items-center gap-3.

video-player-theme.css line 21 applies items-center gap-3 (align-items: center; gap: 12px) to media-control-bar. Since the inline style prop has higher specificity, the CSS @apply values are always overridden and become dead code for those two properties. Consider removing either the alignItems/gap from this object (keeping the CSS @apply) or removing the @apply items-center gap-3 from the CSS (keeping the inline values).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player-theme.ts` around lines 16 - 23, The
CONTROL_BAR_STYLE object currently duplicates the CSS `@apply` items-center gap-3
(alignItems and gap) causing the CSS rules to be overridden; remove the
alignItems and gap properties from the exported CONTROL_BAR_STYLE constant so
the media-control-bar relies on the CSS `@apply` values instead, leaving
paddingBottom/paddingInline/paddingTop intact (ensure the object still casts to
CSSProperties and run checks to confirm layout remains correct).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/VideoPlayer.tsx`:
- Around line 53-57: The YouTube custom element is being rendered before its
definition loads; change the useEffect that imports "youtube-video-element"
(currently watching isYouTube) to set a local state flag (e.g., isYouTubeReady)
only after the import promise resolves, and then render the <youtube-video>
element conditionally based on that flag in the VideoPlayer component; this
ensures the element is only added to the DOM after the module registers the
custom element so MediaController can find a proper media element (keep the
import inside the existing useEffect, set state on success, handle failure by
logging or falling back to a placeholder).

---

Duplicate comments:
In `@src/components/video-player-theme.css`:
- Around line 315-329: The CSS block for selector "media-controller
.settings-menu-wrap media-playback-rate-menu" contains stray blank lines that
trigger stylelint's declaration-empty-line-before rule; remove the empty lines
between declarations (e.g., between "--media-control-padding: 0;" and the
comment, and between "transform: translateX(-50%) !important;" and
"padding-block: 0;") so all declarations are directly adjacent per stylelint
expectations, then reformat to preserve spacing only where allowed by the rule.
- Around line 186-197: Remove the stray empty lines inside the CSS declaration
block containing transform-origin / transform / width / height / backdrop-filter
/ transition / `@apply` (the rule that sets transform-origin: center and
transform: translateY(...)). Ensure there are no blank lines immediately before
the declarations on lines with transform, width, and backdrop-filter so the
declaration-empty-line-before lint rule is satisfied; collapse those blank lines
so all property declarations are contiguous within that selector block.
- Line 231: The CSpell pipeline is flagging custom attribute tokens like
"breakpointmd" and "mediaisfullscreen" used in selectors such as
media-controller[breakpointmd] and the mediaisfullscreen attribute; update the
project's cspell dictionary (e.g., add these tokens to the words array in your
cspell.json or dictionary file) to whitelist breakpointmd and mediaisfullscreen
so the CSS selectors (media-controller[breakpointmd],
media-time-display:not([showduration]), and any
media-controller[mediaisfullscreen]) no longer trigger linting failures.

In `@src/components/VideoPlayer.tsx`:
- Around line 35-40: The prop isActive on VideoPlayerProps is declared but
dropped by the Omit in VideoPlayerInner, so the component never pauses playback
on slide change; restore and use isActive by including it in VideoPlayerInner's
prop type (remove Omit<..., "isActive"> or add isActive to the inner props) and
implement a useEffect inside VideoPlayerInner (or VideoPlayer) that watches
isActive and calls the video element's pause() (via ref) when isActive becomes
false and resumes or does nothing when true; reference VideoPlayerProps,
VideoPlayerInner, isActive and the video element ref to locate where to add this
logic.
- Line 91: The spellchecker flags the attribute value "notooltip" used on
MediaMuteButton; add "notooltip" to the project's cspell dictionary (the words
array in your cspell configuration) so the attribute is recognized, then commit
the updated cspell config; reference the MediaMuteButton usage to locate the
offending token.
- Around line 76-82: The YouTube branch is missing a captions track: update the
JSX in VideoPlayer (the isYouTube branch rendering <youtube-video ... />) to
include a <track> element matching the native <video> branch (e.g., default
kind="captions" label and srcLang attributes) so captions exist for both paths;
keep existing props like ref, slot="media", src and crossOrigin on the
<youtube-video> and add the <track> as a child to satisfy the caption
accessibility requirement.

---

Nitpick comments:
In `@src/components/video-player-theme.css`:
- Around line 203-213: The CSS includes a dead selector "media-controller
.vol-wrap:focus" because .vol-wrap is a non-focusable div; remove that selector
from the rule set so only effective selectors remain (e.g., keep :hover and
:focus-within). Update the rule block that currently lists "media-controller
.vol-wrap:hover, media-controller .vol-wrap:focus, media-controller
.vol-wrap:focus-within, ..." to drop the ".vol-wrap:focus" entry, ensuring the
volume popup visibility is controlled by :hover and :focus-within (which covers
keyboard focus into the MediaVolumeRange).

In `@src/components/video-player-theme.ts`:
- Around line 16-23: The CONTROL_BAR_STYLE object currently duplicates the CSS
`@apply` items-center gap-3 (alignItems and gap) causing the CSS rules to be
overridden; remove the alignItems and gap properties from the exported
CONTROL_BAR_STYLE constant so the media-control-bar relies on the CSS `@apply`
values instead, leaving paddingBottom/paddingInline/paddingTop intact (ensure
the object still casts to CSSProperties and run checks to confirm layout remains
correct).
ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96ed2cf and 1963385.

📒 Files selected for processing (3)
  • src/components/VideoPlayer.tsx
  • src/components/video-player-theme.css
  • src/components/video-player-theme.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/components/VideoPlayer.tsx (2)

57-61: ⚠️ Potential issue | 🟠 Major

<youtube-video> renders before youtube-video-element registers its custom element definition.

useEffect fires post-commit, so the <youtube-video> element is already in the DOM as a plain HTMLElement while the dynamic import is in-flight. MediaController won't find a valid media element until the upgrade completes, leaving controls non-functional during that window.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 57 - 61, The <youtube-video>
element may be inserted before the dynamic import completes, so change the logic
in VideoPlayer to await registration before rendering: in the useEffect that
currently imports "youtube-video-element" (referencing useEffect and isYouTube),
set a local state (e.g., youtubeRegistered) to true only after the import
resolves, and render the <youtube-video> tag conditionally based on that state
so MediaController sees an upgraded/custom element; alternatively, perform the
dynamic import earlier (e.g., on mount) and block rendering until the module is
loaded to ensure the custom element is registered before insertion.

39-44: ⚠️ Potential issue | 🔴 Critical

isActive prop remains unimplemented — video does not pause on lightbox navigation away.

isActive is accepted in VideoPlayerProps and spread into VideoPlayerInner, but VideoPlayerInner omits it via Omit<VideoPlayerProps, "isActive"> and never reads it. The media element will keep playing when the user navigates to another lightbox slide.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 39 - 44, The isActive prop
declared in VideoPlayerProps is never used because VideoPlayerInner is typed
with Omit<VideoPlayerProps, "isActive"> and doesn't read it, causing media to
continue playing when slides change; fix by accepting isActive in
VideoPlayerInner (remove the Omit) or add an explicit isActive parameter to
VideoPlayerInner, then use a ref to the HTMLMediaElement inside VideoPlayerInner
(the same component that renders the <video> or <iframe>) and add an effect that
when isActive becomes false pauses the media (call pause() and optionally set
currentTime or mute) and when true resumes if desired, and ensure VideoPlayer
forwards the isActive prop down to VideoPlayerInner so navigation away actually
stops playback.
🧹 Nitpick comments (1)
src/components/video-player-theme.ts (1)

3-29: Consider migrating static inline-style objects to video-player-theme.css to align with the "no CSS-in-JS" guideline.

Every property in BASE_CONTROLLER_STYLE / CONTROL_BAR_STYLE is static and can live in the CSS file. Two concrete problems with the current approach:

  1. Redundancy: CONTROL_BAR_STYLE sets alignItems: "center" and gap: "12px" (i.e., gap-3), which are the same values already applied in the CSS via @apply items-center gap-3. Inline styles win on specificity, so the @apply line is effectively dead code.
  2. Type unsafe cast: CSS custom properties (--base, --_accent-color, --media-*) are not part of CSSProperties, so the as CSSProperties assertion silently discards type checking. A proper extended type avoids this:
    type MediaControllerCSSProperties = React.CSSProperties & {
      [key: `--${string}`]: string | number;
    };

For the YouTube conditional (aspectRatio, width), a CSS class applied via className is idiomatic:

♻️ Proposed refactor direction

In video-player-theme.css:

 media-controller {
     --video-base: 20px;
+    --media-background-color: `#000`;
+    --media-primary-color: `#fff`;
+    --media-control-background: transparent;
+    --media-control-hover-background: rgb(255 255 255 / 10%);
+    border-radius: 16px;
+    max-height: 80vh;
+    max-width: 1200px;
 }

+media-controller.youtube-player {
+    aspect-ratio: 16/9;
+    width: 100%;
+}

 media-controller media-control-bar {
-    `@apply` items-center gap-3;   /* removed — now explicit below */
+    align-items: center;
+    gap: 12px;
+    background: linear-gradient(transparent, rgb(0 0 0 / 60%));
+    padding-top: 60px;
+    padding-inline: 12px;
+    padding-bottom: 8px;
     touch-action: none;
 }

In VideoPlayer.tsx:

 <MediaController
-    style={isYouTube ? YOUTUBE_CONTROLLER_STYLE : CONTROLLER_STYLE}
+    className={isYouTube ? "youtube-player" : undefined}
     onPointerDownCapture={(event) => event.stopPropagation()}
 >
     ...
-    <MediaControlBar style={CONTROL_BAR_STYLE}>
+    <MediaControlBar>

video-player-theme.ts can then be removed or reduced to nothing.

Based on learnings: "Applies to **/*.{tsx,ts}: Avoid CSS-in-JS runtime styling solutions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player-theme.ts` around lines 3 - 29, Move all static
inline style properties from BASE_CONTROLLER_STYLE, CONTROL_BAR_STYLE, and
YOUTUBE_CONTROLLER_STYLE into video-player-theme.css as CSS classes (e.g.,
.media-controller, .control-bar, .youtube-controller) and replace usages to
apply those classes via className; remove redundant properties that duplicate
`@apply` rules (alignItems/gap) so CSS specificity isn't defeated. Replace the
unsafe as CSSProperties cast by introducing a MediaControllerCSSProperties type
(React.CSSProperties & { [key: `--${string}`]: string | number }) if you must
keep any custom properties in TS, or remove them from the TS file entirely after
migrating to CSS variables in video-player-theme.css. Ensure aspectRatio/width
for YOUTUBE_CONTROLLER_STYLE become a .youtube-controller class and then delete
or reduce video-player-theme.ts accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/VideoPlayer.tsx`:
- Around line 63-76: The callback ref returned from useCallback (the ref
assigned to the video element) currently returns a cleanup that only removes the
error listener, which in React 19 prevents mediaRef(null) from being called on
unmount and blocks MediaStore unregister; update the ref callback (the function
stored in ref) so its cleanup both removes the "error" listener (handleError)
and explicitly calls mediaRef(null) when cleaning up, and ensure the initial
branch that handles el being null still returns undefined; keep using
onErrorRef.current for the handler reference and preserve the existing listener
add/remove logic.

---

Duplicate comments:
In `@src/components/VideoPlayer.tsx`:
- Around line 57-61: The <youtube-video> element may be inserted before the
dynamic import completes, so change the logic in VideoPlayer to await
registration before rendering: in the useEffect that currently imports
"youtube-video-element" (referencing useEffect and isYouTube), set a local state
(e.g., youtubeRegistered) to true only after the import resolves, and render the
<youtube-video> tag conditionally based on that state so MediaController sees an
upgraded/custom element; alternatively, perform the dynamic import earlier
(e.g., on mount) and block rendering until the module is loaded to ensure the
custom element is registered before insertion.
- Around line 39-44: The isActive prop declared in VideoPlayerProps is never
used because VideoPlayerInner is typed with Omit<VideoPlayerProps, "isActive">
and doesn't read it, causing media to continue playing when slides change; fix
by accepting isActive in VideoPlayerInner (remove the Omit) or add an explicit
isActive parameter to VideoPlayerInner, then use a ref to the HTMLMediaElement
inside VideoPlayerInner (the same component that renders the <video> or
<iframe>) and add an effect that when isActive becomes false pauses the media
(call pause() and optionally set currentTime or mute) and when true resumes if
desired, and ensure VideoPlayer forwards the isActive prop down to
VideoPlayerInner so navigation away actually stops playback.

---

Nitpick comments:
In `@src/components/video-player-theme.ts`:
- Around line 3-29: Move all static inline style properties from
BASE_CONTROLLER_STYLE, CONTROL_BAR_STYLE, and YOUTUBE_CONTROLLER_STYLE into
video-player-theme.css as CSS classes (e.g., .media-controller, .control-bar,
.youtube-controller) and replace usages to apply those classes via className;
remove redundant properties that duplicate `@apply` rules (alignItems/gap) so CSS
specificity isn't defeated. Replace the unsafe as CSSProperties cast by
introducing a MediaControllerCSSProperties type (React.CSSProperties & { [key:
`--${string}`]: string | number }) if you must keep any custom properties in TS,
or remove them from the TS file entirely after migrating to CSS variables in
video-player-theme.css. Ensure aspectRatio/width for YOUTUBE_CONTROLLER_STYLE
become a .youtube-controller class and then delete or reduce
video-player-theme.ts accordingly.
ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1963385 and f744741.

📒 Files selected for processing (3)
  • src/components/VideoPlayer.tsx
  • src/components/video-player-theme.css
  • src/components/video-player-theme.ts

Comment on lines +63 to +76
const ref = useCallback(
(el: HTMLVideoElement | null) => {
mediaRef(el);

if (!el) {
return undefined;
}

const handleError = () => onErrorRef.current?.();
el.addEventListener("error", handleError);
return () => el.removeEventListener("error", handleError);
},
[mediaRef],
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

React 19 ref callback cleanup function returns prevents null call unmount

💡 Result:

In React 19, a callback ref may return a cleanup function:

<div
  ref={(node) => {
    if (!node) return;          // (optional) ignore detach
    // setup using node
    return () => {
      // cleanup using the same node
    };
  }}
/>

What changes vs older React:

  • If your ref callback returns a cleanup function, React will call that cleanup when the ref is detached (including unmount), and it will skip calling your ref callback with null on unmount. This is the behavior you’re referring to (“return prevents the null call”). [1][2]
  • For backwards compatibility, if you don’t return a cleanup function, React still uses the old behavior and calls the ref callback with null when detaching—but React says this null-on-detach behavior will be removed in a future version. [2]
  • React also detaches/attaches when the ref callback identity changes (e.g., inline (node) => {} on every render). If you want to avoid unnecessary detach/attach cycles, keep the ref callback stable (e.g., useCallback). [2]

Sources: React 19 announcement + the ref callback docs on react.dev. [1][2]


🌐 Web query:

media-chrome useMediaRef documentation React cleanup null

💡 Result:

useMediaRef() is a React hook exported from media-chrome/react/media-store and is the intended way to “wire up” your <video>/<audio> (or compatible custom media element) to Media Chrome’s MediaStore (so useMediaSelector()/useMediaDispatch() work). You use it by passing it directly to the element’s ref prop. [1]

If you’re seeing cleanup errors like “cannot read … removeEventListener of null”, the common cause is that ref.current is mutable and may be set to null before your effect cleanup runs. The fix is to capture the element in a local variable inside the effect and use that for both add/remove. [2]

const mediaRef = useMediaRef();

useEffect(() => {
  const el = mediaRef.current; // capture once
  if (!el) return;

  const onTimeUpdate = () => {};
  el.addEventListener("timeupdate", onTimeUpdate);

  return () => {
    el.removeEventListener("timeupdate", onTimeUpdate);
  };
}, []);

Sources: Media Chrome “MediaStore React Hooks” docs (includes useMediaRef() usage) [1]; explanation of ref.current becoming null before cleanup + “capture in effect” pattern [2].


🏁 Script executed:

fd VideoPlayer.tsx

Repository: timelessco/recollect

Length of output: 94


🏁 Script executed:

# Once we have the file path, examine it
if [ -f "src/components/VideoPlayer.tsx" ]; then
  cat -n src/components/VideoPlayer.tsx | head -100
fi

Repository: timelessco/recollect

Length of output: 3003


🏁 Script executed:

# Search for media-chrome imports and useMediaRef usage
rg "useMediaRef|media-chrome" -B2 -A2 --type ts --type tsx

Repository: timelessco/recollect

Length of output: 91


🏁 Script executed:

rg "useMediaRef|from.*media-chrome" -t ts -t js

Repository: timelessco/recollect

Length of output: 360


🌐 Web query:

media-chrome MediaStore useMediaRef cleanup null unregister unmount

💡 Result:

In media-chrome/react/media-store, the intended “cleanup/unregister on unmount” behavior is usually driven by the ref callback receiving null (React calls a callback ref with null when the element unmounts or the ref moves).

So the key is: use the callback ref returned by useMediaRef() directly on your media element, and don’t “hide” it behind another ref pattern that prevents the null call from reaching MediaStore.

import { MediaProvider, useMediaRef } from "media-chrome/react/media-store";

function Player() {
  const mediaRef = useMediaRef(); // callback ref

  return (
    <MediaProvider>
      <video ref={mediaRef} />
    </MediaProvider>
  );
}

Common gotchas that can break cleanup:

  • Don’t convert it to an object ref (useRef) and only copy .current; you’ll miss the null callback.
  • Don’t wrap it in a callback that ignores null:
    const mediaRef = useMediaRef();
    const bad = (el) => { if (el) mediaRef(el); }; // blocks cleanup
  • If you need composition, forward null too:
    const mediaRef = useMediaRef();
    const composed = (el) => { mediaRef(el); /* your logic */ };

If you’re chasing leaks specifically: Media Chrome has been actively patching “disconnect/unmount” cleanup (removing listeners, disconnecting observers, etc.) in recent work (merged Feb 19, 2026). If you’re on an older media-chrome version, upgrading may matter. (github.com)

References:

  • media-chrome exports the React media-store entrypoint (./react/media-store). (app.unpkg.com)
  • Example usage of useMediaRef from media-chrome/react/media-store in the wild. (diceui.com)

Citations:


🌐 Web query:

"media-chrome" "useMediaRef" source code implementation state cleanup

💡 Result:

In media-chrome, the React hook useMediaRef() is used as a ref callback you compose onto your <video>/<audio> element so the Media Store can be told when the backing media element changes (example usage shows const mediaRefCallback = useMediaRef(); ... ref={useComposedRefs(..., mediaRefCallback)}), which in turn triggers the store’s “media element changed” flow. [1]

On the Media Store side, the key “state cleanup” logic for custom state wiring lives in createMediaStore()updateStateOwners() inside src/js/media-store/media-store.ts. Each state in the stateMediator can define stateOwnersUpdateHandlers (setup hooks that may return teardown functions). When any “state owner” changes (media element, textTracks, root node, etc.), the store:

  • decides whether it must teardown and/or setup monitoring (teardownSomething / setupSomething),
  • runs teardowns, then runs setups and stores the returned teardown(s). [2]

A bug existed where only the last teardown returned by stateOwnersUpdateHandlers was being kept/called, leaking earlier listeners. That was fixed and merged Nov 17, 2025 (“Ensure all stateOwnersUpdateHandlers are properly torn down”). The fix:

  • stores all teardown functions (if multiple handlers return them),
  • on subsequent updates, normalizes previous teardown(s) into an array and calls every function,
  • filters out non-functions,
  • stores the new teardown(s) as a single function or an array depending on count. [3][2]

Relevant code (current behavior):

  • teardown all previous teardowns (array-or-single) when teardownSomething is true, then
  • setup new handlers and persist teardowns (single-or-array). [2]

Sources: Dice UI docs showing useMediaRef() ref-callback usage pattern [1]; Media Store implementation showing stateOwnersUpdateHandlers teardown/setup logic [2]; PR #1237 describing/fixing the multi-teardown leak and merged date [3].

[1] (diceui.com)
[2] (raw.githubusercontent.com)
[3] (github.com)

Citations:


React 19 ref cleanup skips mediaRef(null)MediaStore unregister is blocked on unmount.

In React 19, returning a cleanup function from a ref callback causes React to call that cleanup on unmount instead of invoking the callback with null. The returned cleanup at line 73 only removes the error listener — it never calls mediaRef(null). This breaks the contract that useMediaRef() from media-chrome/react/media-store relies on: the callback ref must receive null to trigger proper MediaStore cleanup and unregister the element.

The intended pattern for useMediaRef() is to pass it directly as the ref or forward it in a composed ref. When wrapped in a callback that blocks the null call, the MediaStore retains a stale reference to the detached element.

🛠️ Proposed fix
 const ref = useCallback(
     (el: HTMLVideoElement | null) => {
         mediaRef(el);

         if (!el) {
             return undefined;
         }

         const handleError = () => onErrorRef.current?.();
         el.addEventListener("error", handleError);
-        return () => el.removeEventListener("error", handleError);
+        return () => {
+            el.removeEventListener("error", handleError);
+            mediaRef(null);
+        };
     },
     [mediaRef],
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ref = useCallback(
(el: HTMLVideoElement | null) => {
mediaRef(el);
if (!el) {
return undefined;
}
const handleError = () => onErrorRef.current?.();
el.addEventListener("error", handleError);
return () => el.removeEventListener("error", handleError);
},
[mediaRef],
);
const ref = useCallback(
(el: HTMLVideoElement | null) => {
mediaRef(el);
if (!el) {
return undefined;
}
const handleError = () => onErrorRef.current?.();
el.addEventListener("error", handleError);
return () => {
el.removeEventListener("error", handleError);
mediaRef(null);
};
},
[mediaRef],
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 63 - 76, The callback ref
returned from useCallback (the ref assigned to the video element) currently
returns a cleanup that only removes the error listener, which in React 19
prevents mediaRef(null) from being called on unmount and blocks MediaStore
unregister; update the ref callback (the function stored in ref) so its cleanup
both removes the "error" listener (handleError) and explicitly calls
mediaRef(null) when cleaning up, and ensure the initial branch that handles el
being null still returns undefined; keep using onErrorRef.current for the
handler reference and preserve the existing listener add/remove logic.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
src/components/VideoPlayer.tsx (2)

65-78: mediaRef(null) is still not called in the React 19 ref cleanup — MediaStore unregister remains blocked on unmount.

In React 19, returning a cleanup function from a ref callback causes React to invoke that cleanup on unmount instead of re-invoking the callback with null. The cleanup at line 75 only removes the error listener; mediaRef(null) is never called, so the MediaStore retains a stale reference to the detached element. The proposed fix from the previous review (explicitly calling mediaRef(null) inside the cleanup) has not been applied.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 65 - 78, The ref callback
returned by useCallback (the function assigned to ref) currently only removes
the "error" listener in its cleanup, so in React 19 mediaRef(null) is never
called on unmount; update the cleanup returned from the ref callback to both
remove the error listener and explicitly call mediaRef(null) (i.e., ensure the
cleanup calls mediaRef(null) before/after el.removeEventListener), keeping the
existing handleError/onErrorRef.current logic and the useCallback dependency on
mediaRef so MediaStore unregisters the element on unmount.

85-93: <youtube-video> branch is still missing a <track> element for captions.

The native <video> path (line 90–92) includes a <track kind="captions">, but the YouTube path renders <youtube-video> without any caption track. This was flagged in the previous review and remains unaddressed. Based on learnings, "Always include caption tracks for audio and video elements."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 85 - 93, The YouTube branch
renders <youtube-video> without a captions track; update the JSX in
VideoPlayer.tsx so that when isYouTube and youTubeReady render <youtube-video
ref={ref} slot="media" src={src}>, you also include a <track kind="captions"
default label="No captions" srcLang="en" /> (or the same captions source used
for the native <video>) as a child of <youtube-video> to match the native
<video> path; adjust props or children placement on the youtube-video element to
ensure the track is present when youTubeReady is true.
src/components/video-player-theme.css (1)

188-200: Stylelint declaration-empty-line-before errors at lines 192, 195, 197 remain.

Same block and same errors flagged in the previous review — the blank lines between transform, width, backdrop-filter, and transition declarations still violate the declaration-empty-line-before rule and will prevent pnpm fix:css from passing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player-theme.css` around lines 188 - 200, The Stylelint
errors come from extra blank lines before several declarations in the CSS block
(the transform, width, backdrop-filter, and transition declarations); remove the
empty lines so each declaration follows the previous one without a blank line
(e.g., ensure transform-origin/transform, width/height, backdrop-filter, and
transition appear consecutively) to satisfy the declaration-empty-line-before
rule and allow pnpm fix:css to pass.
src/components/lightbox/LightboxRenderers.tsx (1)

77-98: Video playback still not paused on slide navigation — isActive was removed rather than wired.

The previous review flagged that isActive was accepted by VideoPlayer but never used to pause/resume playback. This PR resolves the symptom (removes the dead prop) but not the root behaviour: VideoPlayer has no mechanism to stop playback when the lightbox navigates to a different slide, so the video keeps playing in the background.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lightbox/LightboxRenderers.tsx` around lines 77 - 98,
VideoSlide removed the isActive prop instead of wiring slide-active state
through to VideoPlayer, so videos keep playing when the lightbox navigates;
restore or accept an active flag in VideoSlide and pass it to VideoPlayer (or
derive active from the lightbox context/parent) and update VideoPlayer to
pause/resume playback when that prop changes (useEffect watching isActive and
call the video element's pause()/play() via an internal ref or expose a pause
API). Update VideoSlide to pass the active boolean (e.g., isActive) into the
VideoPlayer call and ensure VideoPlayer implements the pause/resume behavior on
prop change so playback stops when the slide is no longer active.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-player-theme.css`:
- Line 5: Stylelint is flagging Tailwind's `@reference` at-rule and blocking pnpm
fix:css; update the scss/at-rule-no-unknown rule in the Stylelint config to
ignore Tailwind-specific at-rules by adding "reference" to the ignoreAtRules
array for scss/at-rule-no-unknown so `@reference` is whitelisted (keep the rule
active for other at-rules). Locate the scss/at-rule-no-unknown rule in your
Stylelint configuration and append "reference" to its ignoreAtRules option.

In `@src/components/VideoPlayer.tsx`:
- Around line 59-63: The dynamic import of "youtube-video-element" inside the
async IIFE can reject and leaves the player stuck; wrap the import in a
try/catch inside the effect that uses isYouTube, and on error log the exception
and set a failure state so the UI can show a fallback instead of waiting
forever. Concretely, update the block that calls import("youtube-video-element")
/ setYouTubeReady to catch rejects, call setYouTubeReady(false) (and/or a new
setYouTubeError state), log the error (console.error or your app logger), and
ensure MediaController/VideoPlayer renders a graceful fallback or error message
when that error state is set.

---

Duplicate comments:
In `@src/components/lightbox/LightboxRenderers.tsx`:
- Around line 77-98: VideoSlide removed the isActive prop instead of wiring
slide-active state through to VideoPlayer, so videos keep playing when the
lightbox navigates; restore or accept an active flag in VideoSlide and pass it
to VideoPlayer (or derive active from the lightbox context/parent) and update
VideoPlayer to pause/resume playback when that prop changes (useEffect watching
isActive and call the video element's pause()/play() via an internal ref or
expose a pause API). Update VideoSlide to pass the active boolean (e.g.,
isActive) into the VideoPlayer call and ensure VideoPlayer implements the
pause/resume behavior on prop change so playback stops when the slide is no
longer active.

In `@src/components/video-player-theme.css`:
- Around line 188-200: The Stylelint errors come from extra blank lines before
several declarations in the CSS block (the transform, width, backdrop-filter,
and transition declarations); remove the empty lines so each declaration follows
the previous one without a blank line (e.g., ensure transform-origin/transform,
width/height, backdrop-filter, and transition appear consecutively) to satisfy
the declaration-empty-line-before rule and allow pnpm fix:css to pass.

In `@src/components/VideoPlayer.tsx`:
- Around line 65-78: The ref callback returned by useCallback (the function
assigned to ref) currently only removes the "error" listener in its cleanup, so
in React 19 mediaRef(null) is never called on unmount; update the cleanup
returned from the ref callback to both remove the error listener and explicitly
call mediaRef(null) (i.e., ensure the cleanup calls mediaRef(null) before/after
el.removeEventListener), keeping the existing handleError/onErrorRef.current
logic and the useCallback dependency on mediaRef so MediaStore unregisters the
element on unmount.
- Around line 85-93: The YouTube branch renders <youtube-video> without a
captions track; update the JSX in VideoPlayer.tsx so that when isYouTube and
youTubeReady render <youtube-video ref={ref} slot="media" src={src}>, you also
include a <track kind="captions" default label="No captions" srcLang="en" /> (or
the same captions source used for the native <video>) as a child of
<youtube-video> to match the native <video> path; adjust props or children
placement on the youtube-video element to ensure the track is present when
youTubeReady is true.
ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9196e6f and e26c1af.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • package.json
  • src/components/VideoPlayer.tsx
  • src/components/lightbox/LightBox.tsx
  • src/components/lightbox/LightboxRenderers.tsx
  • src/components/video-player-theme.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

* Video Player Theme (media-chrome)
* Scoped to media-controller. Uses project color variables.
*/
@reference "../styles/globals.css";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

@reference blocks pnpm fix:css — add it to the Stylelint ignoreAtRules allowlist.

The scss/at-rule-no-unknown rule (part of the stylelint-scss plugin) flags any at-rule it doesn't recognise as valid SCSS or CSS. @reference is a Tailwind CSS v4 directive that makes utilities available for @apply without emitting output; Stylelint has no knowledge of it. The most direct fix is to explicitly tell Stylelint to ignore Tailwind's at-rules — this preserves the rule for everything else while whitelisting Tailwind-specific ones.

Add "reference" to the scss/at-rule-no-unknown ignoreAtRules option in the project's Stylelint config:

🛠️ Proposed fix (Stylelint config)
 "scss/at-rule-no-unknown": [true, {
   "ignoreAtRules": [
+    "reference",
     "apply",
     "tailwind",
     "layer",
     "theme",
     // …any other existing entries
   ]
 }]
🧰 Tools
🪛 Stylelint (17.3.0)

[error] 5-5: Unexpected unknown at-rule "@reference" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player-theme.css` at line 5, Stylelint is flagging
Tailwind's `@reference` at-rule and blocking pnpm fix:css; update the
scss/at-rule-no-unknown rule in the Stylelint config to ignore Tailwind-specific
at-rules by adding "reference" to the ignoreAtRules array for
scss/at-rule-no-unknown so `@reference` is whitelisted (keep the rule active for
other at-rules). Locate the scss/at-rule-no-unknown rule in your Stylelint
configuration and append "reference" to its ignoreAtRules option.

Comment on lines +59 to +63
void (async () => {
await import("youtube-video-element");
setYouTubeReady(true);
})();
}, [isYouTube]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unhandled failure of youtube-video-element dynamic import — player silently becomes non-functional.

If the dynamic import rejects (network error, etc.), the async IIFE throws inside void (async () => {...})(), the rejection is swallowed, setYouTubeReady(true) is never called, and the user sees an empty/loading MediaController indefinitely with no error feedback or fallback.

🛠️ Proposed fix
-	void (async () => {
-		await import("youtube-video-element");
-		setYouTubeReady(true);
-	})();
+	import("youtube-video-element")
+		.then(() => {
+			setYouTubeReady(true);
+		})
+		.catch(() => {
+			onErrorRef.current?.();
+		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/VideoPlayer.tsx` around lines 59 - 63, The dynamic import of
"youtube-video-element" inside the async IIFE can reject and leaves the player
stuck; wrap the import in a try/catch inside the effect that uses isYouTube, and
on error log the exception and set a failure state so the UI can show a fallback
instead of waiting forever. Concretely, update the block that calls
import("youtube-video-element") / setYouTubeReady to catch rejects, call
setYouTubeReady(false) (and/or a new setYouTubeError state), log the error
(console.error or your app logger), and ensure MediaController/VideoPlayer
renders a graceful fallback or error message when that error state is set.

@rogerantony-dev
Copy link
Contributor Author

Screen.Recording.2026-02-23.at.3.58.35.PM.mov

Video Player Testing

Steps

1. Test native video (file or direct link)

  1. Add a bookmark with a video file or direct video URL (e.g. .mp4, .webm, or a direct link to a video file).
  2. Open the lightbox by clicking the bookmark.
  3. Verify the video player:
    • Play / Pause – toggles playback
    • Mute / Volume – mute button and volume slider work
    • Progress bar – seek by dragging or clicking
    • Time display – current time and duration show correctly
    • Playback rate – settings (gear) menu opens and speed options work
    • PiP (desktop) – picture-in-picture works
    • Fullscreen – fullscreen toggle works

2. Test YouTube video

  1. Add a bookmark with a YouTube URL (e.g. https://www.youtube.com/watch?v=... or https://youtu.be/...).
  2. Open the lightbox by clicking the bookmark.
  3. Confirm the same controls work as for native video:
    • Play / Pause
    • Mute / Volume
    • Progress bar and seeking
    • Time display
    • Playback rate menu
    • Fullscreen

YouTube embeds do not support PiP; that control is hidden for YouTube.

please compare the player against: https://www.interfacecraft.dev/library/using-dial-kit it should match exactly

@rogerantony-dev rogerantony-dev merged commit 3e2dda3 into dev Feb 27, 2026
18 of 20 checks passed
@rogerantony-dev rogerantony-dev deleted the feat/media-chrome-video-player branch February 27, 2026 10:31
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